Jump to content

User:Maxim Masiutin/RefRenamer-core.js

From 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.
// This is a fork from [[User:Nardog/RefRenamer-core.js]] by [[User:Nardog]]
// The fork adds support the pmid (PubMed ID) attribute.

(function refRenamerCore() {
	let messages = Object.assign({
		loadingSource: 'Loading the source...',
		loadingHtml: 'Loading HTML...',
		parsing: 'Parsing wikitext...',
		opening: 'Opening the diff...',
		continue: 'Continue',
		main: 'Main fallback stack:',
		pmid: 'pmid',
		lastName: 'Last name',
		firstName: 'First name',
		author: 'Author',
		periodical: 'Periodical/website',
		publisher: 'Publisher',
		article: 'Article',
		book: 'Book',
		domain: 'Domain',
		firstPhrase: 'First phrase',
		lowercase: 'Lowercase',
		removeDia: 'Remove diacritics',
		removePunct: 'Remove punctuation',
		replaceSpace: 'Replace space with:',
		year: 'Year',
		yearFallback: 'Fall back on any 4-digit number',
		yearConvert: 'Convert to ASCII',
		latinIncrement: 'Use Latin letters for increments',
		latinStart: 'Start with:',
		increment: 'Numeric increments start at:',
		forceIncrement: 'Always insert increments',
		delimiter: 'Delimiter:',
		delimitConditional: 'Insert delimiters only after numerals',
		removeUnreused: 'Remove unreused names',
		apply: 'Apply',
		reset: 'Reset',
		tableName: 'Name',
		tableCaption: 'References to rename',
		tableRef: 'Reference',
		tableNewName: 'New name',
		tableAddRemove: '+/−',
		keepTooltip: 'Uncheck to remove',
		tableRemove: '(Remove)',
		removeTooltip: 'Remove from references to rename',
		otherTableCaption: 'Other named references',
		notReused: '(not reused)',
		addTooltip: 'Add to references to rename',
		addAll: 'Add all',
		resetSelection: 'Reset selection',
		noNamesAlert: 'The source does not contain ref names to rename.',
		noChangesError: 'No names have been modified.',
		duplicatesError: 'The following names are already used or input more than once:',
		summary: 'Replaced [[$1|VE ref names]] using [[$2|RefRenamer]]',
		genericSummary: 'Renamed references using [[$1|RefRenamer]]'
	}, window.refrenamerMessages);
	let getMsg = (key, ...args) => (
		messages.hasOwnProperty(key) ? mw.format(messages[key], ...args) : key
	);
	let notify = key => {
		mw.notify(getMsg(key), {
			autoHideSeconds: 'long',
			tag: 'refrenamer'
		});
	};
	let dialog;
	let encodedPn = encodeURIComponent(mw.config.get('wgPageName'));
	let headers = {
		'Api-User-Agent': 'RefRenamer (https://en.wikipedia.org/wiki/User:Maxim_Masiutin/RefRenamer)'
	};
	window.refRenamer = () => {
		let data = {
			isEdit: document.getElementById('wpTextbox1') &&
				!$('input[name=wpSection]').val(),
			refs: []
		};
		let promise;
		let dependencies = [
			'oojs-ui-windows', 'oojs-ui-widgets', 'jquery.makeCollapsible',
			'oojs-ui.styles.icons-interactions', 'mediawiki.storage'
		];
		if (data.isEdit) {
			dependencies.push('jquery.textSelection');
		} else {
			dependencies.push('mediawiki.api', 'user.options');
			data.started = performance.now();
			notify('loadingSource');
			promise = $.ajax('/w/rest.php/v1/page/' + encodedPn, { headers }).then(response => {
				data.wikitext = response.source;
				data.revId = response.latest.id;
				data.editTime = response.latest.timestamp.replace(/\D/g, '');
			});
		}
		$.when(mw.loader.using(dependencies), promise).then(() => {
			if (data.isEdit) {
				data.wikitext = $('#wpTextbox1').textSelection('getContents');
			}
			let sources = [];
			let wikitext = data.wikitext.replace(/<!--[^]*?-->/g, '');
			let match;
			let re = /<ref\s+name\s*=\s*(?:"\s*([^\n"]+?)\s*"|'?\s*([^\n']+?)\s*'|([^\s>]+?))\s*(?:>([^]+?)<\/ref|\/)>/gi;
			while ((match = re.exec(wikitext))) {
				let name = match[1] || match[2] || match[3];
				let ref = data.refs.find(r => r.name === name);
				if (ref) {
					ref.reused = true;
				} else {
					data.refs.push({
						name: name,
						normalized: normalize(name),
						isVe: /^:\d+$/.test(name)
					});
				}
				if (data.isEdit && match[4]) {
					sources.push(match[0]);
				}
			}
			if (!data.refs.length) throw 'nonames';
			if (data.isEdit) {
				notify('parsing');
				return $.ajax('/api/rest_v1/transform/wikitext/to/html/' + encodedPn, {
					type: 'POST',
					data: { wikitext: sources.join(''), body_only: true },
					headers: headers
				});
			} else {
				notify('loadingHtml');
				return $.ajax('/api/rest_v1/page/html/' + encodedPn, { headers });
			}
		}).then(response => {
			let $page = $($.parseHTML(response));
			let $refs = $page.find('.mw-references:not([data-mw-group]) .mw-reference-text');
			$refs.each(function (i) {
				let match = this.id.match(/^mw-reference-text-cite_note-(.+)-\d+$/);
				if (!match) return;
				let ref = data.refs.find(r => r.normalized === match[1]);
				if (!ref) return;
				ref.coins = {};
				ref.$ref = $refs.eq(i).clone();
				ref.$ref.find('[id], [about]').addBack().removeAttr('id about');
				ref.$ref.find('a').attr('target', '_blank')
					.filter('[href^="./"]').attr('href', function () {
						return mw.format(
							mw.config.get('wgArticlePath'),
							this.getAttribute('href').slice(2)
						);
					});
				let coinsSpan = this.querySelector('.Z3988');
				if (coinsSpan) {
					new URLSearchParams(coinsSpan.title).forEach((v, k) => {
						if (k.startsWith('rft.')) {
							ref.coins[k.slice(4)] = v;
						} else if (k === 'rft_id') {
                     try {
                            if (v.startsWith('http')) {
							   ref.coins.domain = new URL(v).hostname;
                            } else
                            {
                               let infore = /^info:([A-Za-z0-9]+)\/(.+)$/;
                               let infomatch;
                               if ((infomatch = infore.exec(v)) !== null)
                               {
                                 let infokey   = infomatch[1];
                                 let infovalue = infomatch[2];
                                 if (infokey === 'pmid')
                                 {
                                    ref.coins[infokey] = infokey + infovalue;
                                 }
                               } 
                            }
						 } catch (e) {}
						}
					});
				}
				let text = this.textContent;
				if (ref.coins.date) {
					let numbers = ref.coins.date.match(/\p{Nd}+/gu);
					if (numbers) {
						let converted = numbers.map(n => toAscii(n));
						ref.coins.year = numbers[
							converted.indexOf(String(Math.max(...converted)))
						];
						data.hasNonAsciiYear = data.hasNonAsciiYear || isNaN(ref.coins.year);
					}
				} else {
					let yearMarch = text.match(/(?:^|\P{Nd})(\p{Nd}{4})(?!\p{Nd})/u);
					if (yearMarch) {
						ref.year = yearMarch[1];
						data.hasNonAsciiYear = data.hasNonAsciiYear || isNaN(ref.year);
					}
				}
				if (!ref.coins.domain) {
					let link = this.querySelector('a.external');
					if (link) ref.domain = link.hostname;
				}
				ref.phrase = (text.match(/[^\s\p{P}].*?(?=\s*(?:\p{P}|$))/u) || [])[0];
			});
			data.refs = data.refs.filter(ref => ref.$ref);
			if (!data.refs.length) throw 'nonames';
			let collator;
			try {
				collator = Intl.Collator(mw.config.get('wgContentLanguage') + '-u-kn-true');
			} catch (e) {
				collator = Intl.Collator('en-u-kn-true');
			}
			data.refs.sort((a, b) => collator.compare(a.name, b.name));
			if (!dialog) initDialog();
			dialog.open(data);
		}).catch(e => {
			OO.ui.alert(e === 'nonames' ? getMsg('noNamesAlert') : e.message);
		}).always(() => {
			mw.requestIdleCallback(() => {
				let notif = $('.mw-notification-tag-refrenamer').data('mw-notification');
				if (notif) notif.close();
			});
		});
	};
	let initDialog = () => {
		let rtl = document.dir === 'rtl';
		let left = rtl ? 'right' : 'left';
		let right = rtl ? 'left' : 'right';
		mw.loader.addStyleTag(`.refrenamer .oo-ui-window-body{padding:1em} .refrenamer .oo-ui-layout.oo-ui-labelElement.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline{margin:4px 0} .refrenamer-subinput{padding-${left}:2em} .refrenamer-dropdown .oo-ui-fieldLayout-header{flex-grow:0 !important} .refrenamer-dropdown .oo-ui-fieldLayout-field{width:min-content !important} .refrenamer-name, .refrenamer-ref{vertical-align:top} .refrenamer-name{max-width:6em} .refrenamer-name, .refrenamer-ref .mw-reference-text{font-size:90%;overflow-wrap:break-word} .refrenamer-newname{height:3em;position:relative;width:12em} .refrenamer-newname > .oo-ui-textInputWidget{position:absolute;top:0;${left}:0;height:100%} .refrenamer-newname .oo-ui-inputWidget-input{height:100%;resize:none;scrollbar-width:none} .refrenamer-newname .oo-ui-textInputWidget-type-text > .oo-ui-inputWidget-input{font-family:monospace,monospace;font-size:90%} .refrenamer-newname > .oo-ui-checkboxInputWidget{float:${right};margin-${right}:0;z-index:1} .refrenamer-newname .oo-ui-checkboxInputWidget ~ .oo-ui-textInputWidget > .oo-ui-inputWidget-input{padding-${right}:30px} .refrenamer-newname .oo-ui-textInputWidget:not(.oo-ui-element-hidden) + span{display:none} .refrenamer .refrenamer-addremove{padding:0;position:relative;width:32px;height:32px} .refrenamer-addremove > .oo-ui-buttonElement-frameless.oo-ui-iconElement{margin:0;position:absolute;top:0;bottom:0;left:0;right:0} .refrenamer-addremove .oo-ui-buttonElement-button{height:100%} .refrenamer-othername{max-width:32em;overflow-wrap:break-word} .refrenamer-table-wide .refrenamer-othername{max-width:16em}`);
		function RefRenamerDialog(config) {
			RefRenamerDialog.parent.call(this, config);
			this.$element.addClass('refrenamer');
		}
		OO.inheritClass(RefRenamerDialog, OO.ui.ProcessDialog);
		RefRenamerDialog.static.name = 'refRenamerDialog';
		RefRenamerDialog.static.title = 'RefRenamer';
		RefRenamerDialog.static.size = 'large';
		RefRenamerDialog.static.actions = [
			{
				flags: ['safe', 'close']
			},
			{
				action: 'continue',
				label: getMsg('continue'),
				flags: ['primary', 'progressive']
			}
		];
		RefRenamerDialog.prototype.initialize = function () {
			RefRenamerDialog.parent.prototype.initialize.apply(this, arguments);
			this.mainSelect = new OO.ui.MenuTagMultiselectWidget({
				options: [
                    { data: 'pmid', label: getMsg('pmid') },
                    { data: 'aulast', label: getMsg('lastName') },
					{ data: 'aufirst', label: getMsg('firstName') },
					{ data: 'au', label: getMsg('author') },
					{ data: 'jtitle', label: getMsg('periodical') },
					{ data: 'pub|inst', label: getMsg('publisher') },
					{ data: 'atitle|title', label: getMsg('article') },
					{ data: 'btitle', label: getMsg('book') },
					{ data: 'domain', label: getMsg('domain') },
					{ data: 'phrase', label: getMsg('firstPhrase') }
				]
			}).connect(this, { change: 'updateSize', reorder: 'updateSize' });
			this.lowercaseCheck = new OO.ui.CheckboxInputWidget();
			this.removeDiaCheck = new OO.ui.CheckboxInputWidget();
			this.removePunctCheck = new OO.ui.CheckboxInputWidget();
			this.replaceSpaceCheck = new OO.ui.CheckboxInputWidget().on('change', selected => {
				this.replaceSpaceInput.toggle(selected);
				this.updateSize();
			});
			this.replaceSpaceInput = new OO.ui.TextInputWidget({
				classes: ['refrenamer-subinput']
			}).toggle();
			this.yearCheck = new OO.ui.CheckboxInputWidget().on('change', selected => {
				this.yearFallbackLayout.toggle(selected);
				this.yearConvertLayout.toggle(selected && this.hasNonAsciiYear);
				this.latinIncrementLayout.toggle(selected);
				this.updateSize();
			});
			this.yearFallbackCheck = new OO.ui.CheckboxInputWidget();
			this.yearFallbackLayout = new OO.ui.FieldLayout(this.yearFallbackCheck, {
				label: getMsg('yearFallback'),
				align: 'inline',
				classes: ['refrenamer-subinput']
			});
			this.yearConvertCheck = new OO.ui.CheckboxInputWidget();
			this.yearConvertLayout = new OO.ui.FieldLayout(this.yearConvertCheck, {
				label: getMsg('yearConvert'),
				align: 'inline',
				classes: ['refrenamer-subinput']
			});
			this.latinIncrementCheck = new OO.ui.CheckboxInputWidget().on('change', selected => {
				this.latinStartLayout.toggle(selected);
				this.updateSize();
			});
			this.latinIncrementLayout = new OO.ui.FieldLayout(this.latinIncrementCheck, {
				label: getMsg('latinIncrement'),
				align: 'inline',
				classes: ['refrenamer-subinput']
			});
			this.latinIncrementDropdown = new OO.ui.DropdownWidget({
				menu: {
					items: [
						new OO.ui.MenuOptionWidget({ data: 0, label: 'a' }),
						new OO.ui.MenuOptionWidget({ data: 1, label: 'b' })
					]
				}
			});
			this.latinStartLayout = new OO.ui.FieldLayout(this.latinIncrementDropdown, {
				label: getMsg('latinStart'),
				classes: ['refrenamer-subinput', 'refrenamer-dropdown']
			}).toggle();
			this.latinIncrementLayout.$element.append(this.latinStartLayout.$element);
			this.incrementDropdown = new OO.ui.DropdownWidget({
				menu: {
					items: [
						new OO.ui.MenuOptionWidget({ data: 0, label: '0' }),
						new OO.ui.MenuOptionWidget({ data: 1, label: '1' }),
						new OO.ui.MenuOptionWidget({ data: 2, label: '2' })
					]
				}
			});
			this.forceIncrementCheck = new OO.ui.CheckboxInputWidget();
			this.delimiterInput = new OO.ui.TextInputWidget();
			this.delimitConditionalCheck = new OO.ui.CheckboxInputWidget();
			this.removeUnreusedCheck = new OO.ui.CheckboxInputWidget();
			this.applyButton = new OO.ui.ButtonInputWidget({
				label: getMsg('apply'),
				flags: ['progressive'],
				type: 'submit'
			}).connect(this, { click: 'applyConfig' });
			this.resetButton = new OO.ui.ButtonWidget({
				label: getMsg('reset')
			}).connect(this, { click: 'setConfig' });
			this.form = new OO.ui.FormLayout({
				items: [
					new OO.ui.FieldLayout(this.mainSelect, {
						label: getMsg('main'),
						align: 'top'
					}),
					new OO.ui.FieldLayout(this.lowercaseCheck, {
						label: getMsg('lowercase'),
						align: 'inline'
					}),
					new OO.ui.FieldLayout(this.removeDiaCheck, {
						label: getMsg('removeDia'),
						align: 'inline'
					}),
					new OO.ui.FieldLayout(this.removePunctCheck, {
						label: getMsg('removePunct'),
						align: 'inline'
					}),
					new OO.ui.FieldLayout(this.replaceSpaceCheck, {
						label: getMsg('replaceSpace'),
						align: 'inline'
					}),
					this.replaceSpaceInput,
					new OO.ui.FieldLayout(this.yearCheck, {
						label: getMsg('year'),
						align: 'inline'
					}),
					this.yearFallbackLayout,
					this.yearConvertLayout,
					this.latinIncrementLayout,
					new OO.ui.FieldLayout(this.incrementDropdown, {
						label: getMsg('increment'),
						classes: ['refrenamer-dropdown']
					}),
					new OO.ui.FieldLayout(this.forceIncrementCheck, {
						label: getMsg('forceIncrement'),
						align: 'inline'
					}),
					new OO.ui.FieldLayout(this.delimiterInput, {
						label: getMsg('delimiter'),
						align: 'top'
					}),
					new OO.ui.FieldLayout(this.delimitConditionalCheck, {
						label: getMsg('delimitConditional'),
						align: 'inline'
					}),
					new OO.ui.FieldLayout(this.removeUnreusedCheck, {
						label: getMsg('removeUnreused'),
						align: 'inline'
					}),
					this.applyButton,
					this.resetButton
				]
			});
			this.$tbody = $('<tbody>');
			this.$otherTbody = $('<tbody>');
			this.$otherTable = $('<table>').addClass('wikitable').append(
				$('<caption>').text(getMsg('otherTableCaption')),
				$('<thead>').append(
					$('<tr>').append(
						$('<th>').text(getMsg('tableName')),
						$('<th>').text(getMsg('tableRef')),
						$('<th>').text(getMsg('tableAddRemove'))
					)
				),
				this.$otherTbody
			).on('afterExpand.mw-collapsible', function () {
				this.classList.add('refrenamer-table-wide');
			}).on('afterCollapse.mw-collapsible', function () {
				this.classList.toggle(
					'refrenamer-table-wide',
					!!this.querySelector('.mw-collapsible:not(.mw-collapsed)')
				);
			});
			this.addAllButton = new OO.ui.ButtonWidget({
				flags: ['progressive'],
				label: getMsg('addAll')
			}).on('click', () => {
				this.refs.forEach(ref => {
					if (!ref.renamable) {
						this.addCandidate(ref, true);
					}
				});
			});
			this.resetSelectionButton = new OO.ui.ButtonWidget({
				label: getMsg('resetSelection')
			}).on('click', () => {
				this.refs.forEach(ref => {
					if (ref.renamable) {
						if (!ref.isVe) this.removeCandidate(ref);
					} else {
						if (ref.isVe) this.addCandidate(ref, true);
					}
				});
			});
			this.$body.append(
				this.form.$element,
				$('<table>').addClass('wikitable').append(
					$('<caption>').text(getMsg('tableCaption')),
					$('<thead>').append(
						$('<tr>').append(
							$('<th>').text(getMsg('tableName')),
							$('<th>').text(getMsg('tableRef')),
							$('<th>').text(getMsg('tableNewName')),
							$('<th>').text(getMsg('tableAddRemove'))
						)
					),
					this.$tbody
				),
				this.$otherTable,
				this.addAllButton.$element,
				this.resetSelectionButton.$element
			);
			this.defaults = {
				main: ['aulast', 'aufirst', 'au', 'jtitle', 'pub|inst', 'phrase'],
 				lowercase: false,
                removeDia: false,
				removePunct: false,
				replaceSpace: false,
				year: true,
				yearFallback: false,
				yearConvert: true,
				latinIncrement: false,
				increment: 2,
				forceIncrement: false,
				delimiter: '-',
				delimitConditional: false,
				removeUnreused: true
			};
		};
		RefRenamerDialog.prototype.getConfig = function () {
			return {
				main: this.mainSelect.getValue(),
				lowercase: this.lowercaseCheck.isSelected(),
				removeDia: this.removeDiaCheck.isSelected(),
				removePunct: this.removePunctCheck.isSelected(),
				replaceSpace: this.replaceSpaceCheck.isSelected() &&
				this.replaceSpaceInput.getValue(),
				year: this.yearCheck.isSelected(),
				yearFallback: this.yearFallbackCheck.isSelected(),
				yearConvert: this.yearConvertCheck.isSelected(),
				latinIncrement: this.latinIncrementCheck.isSelected() &&
				this.latinIncrementDropdown.getMenu().findSelectedItem().getData(),
				increment: this.incrementDropdown.getMenu().findSelectedItem().getData(),
				forceIncrement: this.forceIncrementCheck.isSelected(),
				delimiter: this.delimiterInput.getValue(),
				delimitConditional: this.delimitConditionalCheck.isSelected(),
				removeUnreused: this.removeUnreusedCheck.isSelected()
			};
		};
		RefRenamerDialog.prototype.setConfig = function (config) {
			config = Object.assign({}, this.defaults, config);
			this.mainSelect.setValue(config.main);
			this.lowercaseCheck.setSelected(config.lowercase);
			this.removeDiaCheck.setSelected(config.removeDia);
			this.removePunctCheck.setSelected(config.removePunct);
			this.replaceSpaceCheck.setSelected(typeof config.replaceSpace === 'string');
			this.replaceSpaceInput.setValue(
				this.replaceSpaceCheck.isSelected() ? config.replaceSpace : '-'
			);
			this.yearCheck.setSelected(config.year);
			this.yearFallbackCheck.setSelected(config.yearFallback);
			this.yearConvertCheck.setSelected(config.yearConvert);
			this.latinIncrementCheck.setSelected(typeof config.latinIncrement === 'number');
			this.latinIncrementDropdown.getMenu().selectItemByData(
				this.latinIncrementCheck.isSelected() ? config.latinIncrement : 0
			);
			this.incrementDropdown.getMenu().selectItemByData(config.increment);
			this.forceIncrementCheck.setSelected(config.forceIncrement);
			this.delimiterInput.setValue(config.delimiter);
			this.delimitConditionalCheck.setSelected(config.delimitConditional);
			this.removeUnreusedCheck.setSelected(config.removeUnreused);
		};
		RefRenamerDialog.prototype.applyConfig = function () {
			let config = this.getConfig();
			config.processed = this.refs.filter(ref => !ref.renamable)
				.map(ref => ref.normalized);
			this.refs.forEach(ref => {
				if (ref.renamable) {
					this.applyConfigTo(ref, config);
				}
			});
		};
		RefRenamerDialog.prototype.applyConfigTo = function (ref, config) {
			config = config || this.getConfig();
			if (!config.processed) {
				config.processed = this.refs.filter(r => r !== ref).map(r => (
					r.renamable
						? normalize(r.input.getValue()) || r.normalized
						: r.normalized
				));
			}
			ref.input.setValue('');
			if (!ref.reused) {
				ref.keepCheck.setSelected(!config.removeUnreused);
				if (config.removeUnreused) return;
			}
			let s;
			config.main.some(key => key.split('|').some(subkey => {
				s = ref.coins[subkey] || ref[subkey];
				return s;
			}));
			if (!s) {
				config.processed.push(ref.normalized);
				return;
			}
			if (config.lowercase) {
				s = s.toLowerCase();
			}
			if (config.removeDia) {
				s = s.normalize('NFD').replace(/\p{Mn}/gu, '');
			}
			if (config.removePunct) {
				s = s.replace(/\p{P}/gu, '');
			}
			if (typeof config.replaceSpace === 'string') {
				s = s.replace(/\s+/g, config.replaceSpace);
			}
			let useLatin;
			let year;
            if (!s.startsWith('pmid'))
            {
              year = config.year &&
				(ref.coins.year || config.yearFallback && ref.year); 
            }
			if (year) {
				if (config.yearConvert) year = toAscii(year);
				let delimiter = config.delimitConditional && /\P{Nd}$/u.test(s)
					? ''
					: config.delimiter;
				s += delimiter + year;
				useLatin = typeof config.latinIncrement === 'number';
			}
			let unsuffixed = s;
			let normalized = normalize(s);
			let delimiter = useLatin || (
				config.delimitConditional && /\P{Nd}$/u.test(unsuffixed)
			) ? '' : config.delimiter;
			let increment = useLatin ? config.latinIncrement : config.increment;
			let incrementStr = useLatin ? toLatin(increment) : increment;
			let forceIncrement = config.forceIncrement;
			while (forceIncrement || config.processed.includes(normalized)) {
				s = unsuffixed + delimiter + incrementStr;
				normalized = normalize(s);
				increment++;
				incrementStr = useLatin ? toLatin(increment) : increment;
				forceIncrement = false;
			}
			config.processed.push(normalized);
			ref.input.setValue(s);
		};
		RefRenamerDialog.prototype.addCandidate = function (ref, isMove) {
			ref.renamable = true;
			if (this.refs.every(r => r.renamable)) {
				this.$otherTable.hide();
				this.addAllButton.toggle(false);
			} else {
				this.$otherTable.show();
				this.addAllButton.toggle(true);
			}
			ref.$otherRow.hide();
			if (isMove) {
				ref.$otherRow.find('.mw-collapsible')
					.data('mw-collapsible').collapse();
			}
			if (ref.removeButton) {
				this.applyConfigTo(ref);
				ref.$row.children('.refrenamer-ref').append(ref.$ref);
				ref.$row.show();
				return;
			}
			ref.input = new OO.ui.MultilineTextInputWidget({
				allowLinebreaks: false,
				placeholder: ref.name
			}).connect(this, { enter: ['executeAction', 'continue'] });
			ref.input.$input.on({ focus: onFocus, blur: onBlur });
			if (!ref.reused) {
				ref.keepCheck = new OO.ui.CheckboxInputWidget({
					selected: true,
					title: getMsg('keepTooltip')
				}).connect(ref.input, { change: 'toggle' });
				ref.keepCheck.$input.on({ focus: onFocus, blur: onBlur });
			}
			if (isMove) this.applyConfigTo(ref);
			ref.removeButton = new OO.ui.ButtonWidget({
				flags: ['destructive'],
				framed: false,
				icon: 'subtract',
				title: getMsg('removeTooltip')
			}).connect(this, { click: ['removeCandidate', ref] });
			ref.$row.append(
				$('<td>').addClass('refrenamer-name').text(ref.name),
				$('<td>').addClass('refrenamer-ref mw-parser-output').append(ref.$ref),
				$('<td>').addClass('refrenamer-newname').append(
					ref.keepCheck && ref.keepCheck.$element,
					ref.input.$element,
					ref.keepCheck && $('<span>').text(getMsg('tableRemove'))
				),
				$('<td>').addClass('refrenamer-addremove').append(ref.removeButton.$element)
			).show();
		};
		RefRenamerDialog.prototype.removeCandidate = function (ref) {
			ref.renamable = false;
			this.$otherTable.show();
			this.addAllButton.toggle(true);
			ref.$row.hide();
			if (ref.addButton) {
				ref.$otherRow.find('.mw-collapsible-content').append(ref.$ref);
				ref.$otherRow.show();
				return;
			}
			ref.addButton = new OO.ui.ButtonWidget({
				flags: ['progressive'],
				framed: false,
				icon: 'add',
				title: getMsg('addTooltip')
			}).connect(this, { click: ['addCandidate', ref, true] });
			ref.$otherRow.append(
				$('<td>').addClass('refrenamer-othername').text(ref.name).append(!ref.reused && [
					' ',
					$('<small>').text(getMsg('notReused'))
				]),
				$('<td>').addClass('refrenamer-ref mw-parser-output').append(
					$('<div>').addClass('mw-collapsible mw-collapsed')
						.append(ref.$ref).makeCollapsible()
				),
				$('<td>').addClass('refrenamer-addremove').append(ref.addButton.$element)
			).show();
		};
		RefRenamerDialog.prototype.getSetupProcess = function (data) {
			Object.assign(this, data);
			this.$tbody.empty();
			this.$otherTbody.empty();
			this.refs.forEach(ref => {
				ref.$row = $('<tr>').appendTo(this.$tbody);
				ref.$otherRow = $('<tr>').appendTo(this.$otherTbody);
				this[ref.isVe ? 'addCandidate' : 'removeCandidate'](ref);
			});
			this.resetSelectionButton.toggle(this.refs.some(ref => !ref.isVe));
			this.setConfig(mw.storage.getObject('refrenamer'));
			this.applyConfig();
			this.$body.scrollTop(0);
			return RefRenamerDialog.super.prototype.getSetupProcess.call(this, data);
		};
		RefRenamerDialog.prototype.getActionProcess = function (action) {
			return RefRenamerDialog.super.prototype.getActionProcess.call(this, action).next(function () {
				if (action !== 'continue') return;
				let removed = [];
				let modified = this.refs.filter(ref => {
					if (!ref.renamable) return;
					if (ref.keepCheck && !ref.keepCheck.isSelected()) {
						removed.push(ref);
						return;
					}
					ref.newName = ref.input.getValue().replace(/^[\s_]+|[\s_]+$/g, '');
					ref.newNormalized = normalize(ref.newName);
					return ref.newNormalized;
				});
				if (!modified.length && !removed.length) {
					return new OO.ui.Error(getMsg('noChangesError'), { recoverable: false });
				}
				let duplicates = new Set(
					this.refs.map(ref => (
						ref.renamable
							? ref.newNormalized || ref.normalized
							: ref.normalized
					)).filter((name, i, arr) => arr.indexOf(name) !== i)
				);
				if (duplicates.size) {
					return new OO.ui.Error($([
						document.createTextNode(getMsg('duplicatesError')),
						$('<ul>').append([...duplicates].map(n => $('<li>').text(n)))[0]
					]), { recoverable: false });
				}
				this.close();
				let subs = {};
				[...modified, ...removed].forEach(ref => {
					subs[ref.name] = ref.newName;
				});
				let newText = replace(this.wikitext, subs);
				let iw = mw.config.get('wgWikiID') === 'enwiki' ? '' : 'w:en:';
				let summary = [...modified, ...removed].every(ref => ref.isVe) ? getMsg(
					'summary',
					iw + 'Wikipedia:VisualEditor/Named references',
					iw + 'User:Maxim Masiutin/RefRenamer'
				) : getMsg('genericSummary', iw + 'User:Maxim Masiutin/RefRenamer');
				if (this.isEdit) {
					$('#wpTextbox1').textSelection('setContents', newText);
					if (document.documentElement.classList.contains('ve-active')) {
						ve.init.target.once('showChanges', () => {
							ve.init.target.saveDialog.reviewModeButtonSelect.selectItemByData('source');
							ve.init.target.saveDialog.setEditSummary(summary);
						});
						ve.init.target.showSaveDialog('review');
					} else {
						$('#wpSummary').textSelection('setContents', summary);
						$('#wpDiff').trigger('click');
					}
				} else {
					new mw.Api().get({
						action: 'query',
						titles: mw.config.get('wgPageName'),
						prop: 'info',
						inprop: 'watched',
						curtimestamp: 1,
						formatversion: 2
					}).always(response => {
						let formData = [];
						let timestamp = response && response.curtimestamp;
						if (timestamp) {
							let elapsed = performance.now() - this.started;
							let time = new Date(new Date(timestamp).getTime() - elapsed)
								.toISOString().slice(0, -5).replace(/\D/g, '');
							formData.push(['wpStarttime', time]);
						}
						formData.push(
							['wpEdittime', this.editTime],
							['editRevId', this.revId],
							['wpTextbox1', newText],
							['wpSummary', summary],
							['wpMinoredit', '']
						);
						let page = (((response || {}).query || {}).pages || [])[0] || {};
						if (page.watched ||
							Number(mw.user.options.get('watchdefault')) === 1
						) {
							formData.push(['wpWatchthis', '']);
							if (page.watchlistexpiry) {
								formData.push(['wpWatchlistExpiry', page.watchlistexpiry]);
							}
						}
						formData.push(['wpDiff', ''], ['wpUltimateParam', 1]);
						$('<form>').attr({
							method: 'post',
							action: mw.util.getUrl(null, { action: 'submit' }),
							enctype: 'multipart/form-data'
						}).append(
							formData.map(([n, v]) => $('<input>').attr({
								name: n,
								type: 'hidden'
							}).val(v))
						).appendTo(document.body).trigger('submit').remove();
					});
					notify('opening');
				}
				mw.requestIdleCallback(() => {
					let customized = Object.entries(this.getConfig())
						.filter(([k, v]) => String(v) !== String(this.defaults[k]));
					if (customized.length) {
						mw.storage.setObject('refrenamer', Object.fromEntries(customized), 7776000);
					} else {
						mw.storage.remove('refrenamer');
					}
				});
			}, this);
		};
		RefRenamerDialog.prototype.hideErrors = function () {
			RefRenamerDialog.super.prototype.hideErrors.call(this);
			this.actions.setAbilities({ continue: true });
		};
		dialog = new RefRenamerDialog();
		let winMan = new OO.ui.WindowManager();
		winMan.addWindows([dialog]);
		winMan.$element.appendTo(OO.ui.getTeleportTarget());
	};
	let replace = (text, subs) => text.replace(
		/<(ref)\s+(name)\s*=\s*(?:"\s*([^\n"]+?)\s*"|'\s*([^\n']+?)\s*'|([^\s>]+?))(\s*\/?)>/gi,
		(s, ref, attr, name1, name2, name3, slash) => {
			let name = name1 || name2 || name3;
			return subs.hasOwnProperty(name)
				? subs[name]
					? `<${ref} ${attr}="${subs[name]}"${slash}>`
					: `<${ref}>`
				: s;
		}
	);
	let normalize = s => s.replace(/[\s_]+/g, '_').replace(/^_+|_+$/g, '');
	let toAscii = s => s.replace(/\D/g, c => {
		let cp = c.codePointAt(0);
		let zero = cp;
		while (/\p{Nd}/u.test(String.fromCodePoint(zero - 1))) {
			zero--;
		}
		return (cp - zero) % 10;
	});
	let toLatin = n => {
		let s = '';
		do {
			s = String.fromCharCode(97 + (n % 26)) + s;
			n = Math.floor(n / 26) - 1;
		} while (n >= 0);
		return s;
	};
	let onFocus = () => {
		dialog.refs.forEach(ref => {
			if (ref.renamable) {
				ref.input.setTabIndex(1);
				if (ref.keepCheck) ref.keepCheck.setTabIndex(1);
			}
		});
	};
	let onBlur = () => {
		dialog.refs.forEach(ref => {
			if (ref.renamable) {
				ref.input.setTabIndex(0);
				if (ref.keepCheck) ref.keepCheck.setTabIndex(0);
			}
		});
	};
	window.refRenamer();
}());