Jump to content

User:Nardog/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.
(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:',
		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: 'Append Latin letters on collision',
		increment: 'Collision resolution:',
		incrementExample: 'Example',
		incrementExamples: '$1, $2...',
		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: '±',
		reapplyTooltip: 'Reapply current options',
		propsTooltip: 'View/insert properties',
		keepTooltip: 'Uncheck to remove',
		tableRemove: '(Remove)',
		removeTooltip: 'Remove from references to rename',
		otherTableCaption: 'Other named references',
		notReused: '(not reused)',
		expand: 'Expand',
		collapse: 'Collapse',
		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.',
		numericError: 'The following names are invalid as they consist only of numerals:',
		duplicatesError: 'The following names are already used or input more than once:',
		templatesWarn: 'Ref names in the following templates will not be replaced:',
		invalidWarn: 'The following names have been ignored because it could not be determined which references correspond to them:',
		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 notif;
	let notify = key => {
		mw.notify(getMsg(key), {
			autoHideSeconds: 'long',
			tag: 'refrenamer'
		}).then(n => {
			notif = n;
		});
	};
	let dialog;
	class Ref {
		constructor(name, normalized) {
			this.name = name;
			this.names = new Set([name]);
			this.normalized = normalized;
			this.isVe = /^:\d+$/.test(name);
			this.isAuto = this.isVe || /^auto(?:generated)?\d*$/.test(name);
		}
		initProps() {
			this.props = {};
			let coinsSpan = this.$ref[0].querySelector('.Z3988');
			if (coinsSpan) {
				new URLSearchParams(coinsSpan.title).forEach((v, k) => {
					if (k.startsWith('rft.')) {
						this.props[k.slice(4)] = v;
					} else if (k === 'rft_id') {
						if (/^https?:/.test(v)) {
							if (this.props.domain) return;
							try {
								let url = new URL(v);
								this.props.domain = url.hostname;
							} catch (e) {}
						} else {
							let match = v.match(/^info:([^\/]+)\/(.+)$/);
							if (match) {
								this.props[match[1]] = match[2];
							}
						}
					}
				});
			}
			let text = this.$ref.text();
			if (this.props.date) {
				let numbers = this.props.date.match(/\p{Nd}+/gu);
				if (numbers) {
					let converted = numbers.map(n => toAscii(n));
					let year = String(Math.max(...converted));
					let original = numbers[converted.indexOf(year)];
					this.props.year = original;
					if (original !== year) {
						this.props.yearAscii = year;
					}
				}
			} else {
				let match = text.match(/(?:^|\P{Nd})(\p{Nd}{4})(?!\p{Nd})/u);
				if (match) {
					this.props.textYear = match[1];
					let ascii = toAscii(match[1]);
					if (ascii !== match[1]) {
						this.props.textYearAscii = ascii;
					}
				}
			}
			let link = this.$ref[0].querySelector('a.external');
			if (link && link.hostname !== this.props.domain) {
				this.props.linkDomain = link.hostname;
			}
			let match = text.match(/[^\s\p{P}].*?(?=\s*(?:\p{P}|$))/u);
			if (match) {
				this.props.phrase = match[0];
			}
			Object.freeze(this.props);
		}
		initRows() {
			let rowClass = !this.reused && 'refrenamer-unreused';
			this.$row = $('<tr>').addClass(rowClass).appendTo(dialog.$tbody);
			this.$otherRow = $('<tr>').addClass(rowClass).appendTo(dialog.$otherTbody);
			this.nameCell = $('<td>').addClass('refrenamer-name')
				.append($('<span>').text(this.name))[0];
			this.refCell = $('<td>').addClass('refrenamer-ref mw-parser-output')
				.append(this.$ref)[0];
			this.moveButton = new OO.ui.ButtonWidget({
				framed: false,
				invisibleLabel: true
			}).connect(dialog, { click: ['toggleActive', this] });
			this.moveCell = $('<td>').addClass('refrenamer-addremove')
				.append(this.moveButton.$element)[0];
		}
		setActive(active) {
			if (active === this.active) return;
			this.active = active;
			this[active ? '$otherRow' : '$row'].addClass('refrenamer-hidden');
			let tooltip = getMsg(active ? 'removeTooltip' : 'addTooltip');
			this.moveButton
				.setFlags({ destructive: active, progressive: !active })
				.setIcon(active ? 'subtract' : 'add')
				.setLabel(tooltip).setTitle(tooltip);
			this[active ? 'initInput' : 'initToggle']();
			this[active ? '$row' : '$otherRow']
				.prepend(this.nameCell, this.refCell)
				.append(this.moveCell)
				.removeClass('refrenamer-hidden');
		}
		initInput() {
			if (this.input) return;
			this.input = new OO.ui.MultilineTextInputWidget({
				allowLinebreaks: false,
				placeholder: this.name,
				spellcheck: false
			}).connect(dialog, { enter: ['executeAction', 'continue'] });
			this.reapplyButton = new OO.ui.ButtonWidget({
				classes: ['refrenamer-reapplybutton'],
				framed: false,
				icon: 'undo',
				invisibleLabel: true,
				label: getMsg('reapplyTooltip')
			}).connect(dialog, {
				click: ['applyConfig', [this], true]
			}).connect(this.input, { click: 'focus' });
			this.propsButton = new OO.ui.ButtonMenuSelectWidget({
				classes: ['refrenamer-propsbutton'],
				clearOnSelect: true,
				framed: false,
				icon: 'downTriangle',
				invisibleLabel: true,
				label: getMsg('propsTooltip'),
				menu: {
					$floatableContainer: this.input.$element,
					horizontalPosition: 'end',
					width: '16em'
				}
			});
			this.propsButton.getMenu().addItems(
				Object.entries(this.props).map(([k, v]) => {
					let option = new OO.ui.MenuOptionWidget({
						label: v,
						title: v
					});
					option.$label.attr('data-refrenamer', k);
					return option;
				})
			).connect(this, { choose: 'onPropChoose' });
			if (!this.reused) {
				this.keepCheck = new OO.ui.CheckboxInputWidget({
					classes: ['refrenamer-keepcheck'],
					invisibleLabel: true,
					label: getMsg('keepTooltip')
				}).connect(this, { change: 'onKeepChange' });
			}
			$('<td>').addClass('refrenamer-newname').append(
				this.keepCheck && this.keepCheck.$element,
				this.input.$element,
				this.reapplyButton.$element,
				this.propsButton.$element
			).appendTo(this.$row);
		}
		initToggle() {
			if (this.$toggle) return;
			this.$toggle = $('<button>').attr({
				class: 'refrenamer-toggle oo-ui-icon-expand',
				title: getMsg('expand')
			}).on('click', onToggle).prependTo(this.refCell);
		}
		onPropChoose(item) {
			this.input.insertContent(item.getTitle());
		}
		onKeepChange(selected) {
			this.$row.toggleClass('refrenamer-kept', selected);
			if (selected && !this.input.getValue()) {
				dialog.applyConfig([this], true);
			}
		}
		setupCollapsible(width) {
			if (this.collapsible && Math.abs(width - this.refWidth) < 10) return;
			this.refCell.classList.remove('refrenamer-collapsible');
			this.collapsible = this.$ref[0].scrollHeight > this.$ref[0].clientHeight;
			this.refCell.classList.toggle('refrenamer-collapsible', this.collapsible);
			this.refWidth = width;
		}
	}
	window.refRenamer = () => {
		let encodedPn = encodeURIComponent(mw.config.get('wgPageName'));
		let headers = {
			'Api-User-Agent': 'RefRenamer (https://en.wikipedia.org/wiki/User:Nardog/RefRenamer)'
		};
		let dependencies = [
			'mediawiki.storage', 'mediawiki.Title', 'oojs-ui-windows',
			'oojs-ui-widgets', 'oojs-ui.styles.icons-interactions',
			'oojs-ui.styles.icons-movement', 'oojs-ui.styles.icons-content',
			'oojs-ui.styles.icons-editing-core'
		];
		let data = {
			isEdit: !!document.getElementById('wpTextbox1') &&
				!$('input[name=wpSection]').val(),
			refs: [],
			templates: new Set(),
			invalid: new Set()
		};
		let promise;
		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 wikitext = data.wikitext.replace(/<!--[^]*?-->/g, '');
			let match;
			let re = /<ref\s+(?:name|follow)\s*=\s*(?:"\s*([^\n"]+?)\s*"?|'\s*([^\n']+?)\s*'?|([^\s>]+?))\s*\/?>/gi;
			while ((match = re.exec(wikitext))) {
				let name = match[1] || match[2] || match[3];
				let normalized = normalize(name);
				let ref = data.refs.find(r => r.normalized === normalized);
				if (ref) {
					ref.reused = true;
					ref.names.add(name);
				} else {
					data.refs.push(new Ref(name, normalized));
				}
			}
			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: data.wikitext, body_only: true },
					headers: headers
				});
			} else {
				notify('loadingHtml');
				return $.ajax('/api/rest_v1/page/html/' + encodedPn, { headers });
			}
		}).then(response => {
			let numbers = new Set();
			let $page = $($.parseHTML(response));
			$page.find(
				'.mw-references:not([data-mw-group]) .mw-reference-text'
			).each(function () {
				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;
				if (ref.$ref) {
					data.invalid.add(ref.name);
					ref.invalid = true;
					return;
				}
				ref.$ref = $(this);
				ref.reused = ref.reused || (
					ref.$ref.prev('span[rel="mw:referencedBy"]').children().length > 1
				);
				ref.$ref.remove().find('[id], [about]').addBack().removeAttr('id about');
				ref.$ref.find('a').attr('target', '_blank')
					.filter('[href^="./"]').attr('href', (_, href) => (
						mw.format(mw.config.get('wgArticlePath'), href.slice(2))
					));
				numbers.add(match[2]);
			});
			$page.find(
				'.mw-ref[typeof~="mw:Transclusion"], [typeof~="mw:Transclusion"] .mw-ref'
			).filter(function () {
				if (!/^\[\d+\]$/.test(this.textContent)) return;
				let match = this.id.match(/_(\d+)-\d+$/);
				return match && numbers.has(match[1]);
			}).closest('[typeof~="mw:Transclusion"]').each(function () {
				try {
					data.templates.add(
						JSON.parse(this.dataset.mw).parts[0].template.target.href
							.replace(/^.\/Template:/, '')
					);
				} catch (e) {}
			});
			data.refs = data.refs.filter(ref => ref.$ref && !ref.invalid);
			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(() => {
			if (notif) {
				notif.close();
				notif = null;
			}
		});
	};
	let initDialog = () => {
		let rtl = document.dir === 'rtl';
		let left = rtl ? 'right' : 'left';
		let right = rtl ? 'left' : 'right';
		mw.loader.addStyleTag(`.refrenamer .oo-ui-tabOptionWidget > .oo-ui-labelElement-label::after{content:" (" attr(data-refrenamer) ")"} .refrenamer-split{columns:2} .refrenamer-split .oo-ui-fieldLayout{break-inside:avoid} .refrenamer-split, .refrenamer .oo-ui-layout.oo-ui-labelElement:nth-child(n+2), .refrenamer .oo-ui-fieldLayout-align-inline .oo-ui-labelElement-label + *{margin:4px 0} .refrenamer .oo-ui-textInputWidget > .oo-ui-inputWidget-input{font-family:monospace,monospace} .refrenamer .wikitable{margin-bottom:0;overflow-wrap:break-word;width:100%} .refrenamer-maintable .refrenamer-name, .refrenamer .mw-reference-text, .refrenamer-othertable .refrenamer-name::after{font-size:90%} .refrenamer-unreused > .refrenamer-name::after{content:"${getMsg('notReused')}";display:inline-block} .refrenamer-unreused > .refrenamer-name > span::after{content:" "} .refrenamer-hidden, .refrenamer-unreused:not(.refrenamer-kept) > .refrenamer-newname > :not(.refrenamer-keepcheck), .refrenamer-maintable .refrenamer-toggle, :not(.refrenamer-collapsible) > .refrenamer-toggle{display:none} .refrenamer-name, .refrenamer-ref{vertical-align:top} .refrenamer-maintable .refrenamer-name{max-width:6em} .refrenamer-ref{line-height:normal} .refrenamer-newname{height:3em;position:relative;width:12em} .refrenamer-unreused:not(.refrenamer-kept) > .refrenamer-newname::after{content:"${getMsg('tableRemove')}"} .refrenamer-newname > .oo-ui-textInputWidget, .refrenamer-addremove > .oo-ui-buttonElement-frameless.oo-ui-iconElement, .refrenamer-toggle{margin:0;position:absolute;top:0;bottom:0;left:0;right:0} .refrenamer-newname textarea{height:100%;resize:none} .refrenamer-keepcheck{position:absolute;${right}:4px;top:50%;transform:translateY(-50%);margin:0;z-index:1} .refrenamer-keepcheck + .oo-ui-textInputWidget > textarea{padding-${right}:28px} .refrenamer-newname .oo-ui-buttonElement-frameless{position:absolute;${right}:0;bottom:0;margin:0} .refrenamer .refrenamer-reapplybutton{${right}:24px} .refrenamer-newname:not(:hover):not(:focus-within) > .oo-ui-buttonElement-frameless{opacity:0} .refrenamer-newname .oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button{min-width:24px;min-height:24px;padding:0} .refrenamer-newname .oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon{background-size:16px 16px;left:0;right:0;margin:auto} .refrenamer-propsbutton .oo-ui-menuOptionWidget{font-size:85%;padding:2px 8px} .refrenamer-propsbutton .oo-ui-menuOptionWidget > .oo-ui-labelElement-label::before{content:attr(data-refrenamer);color:var(--color-subtle,#54595d);float:${right}} .refrenamer .refrenamer-addremove{padding:0;position:relative;width:32px;height:32px} .refrenamer-addremove .oo-ui-buttonElement-button{height:100%} .refrenamer-othertable{margin:0} .refrenamer-othertable .refrenamer-name{max-width:16em} .refrenamer-othertable .refrenamer-ref{position:relative} .refrenamer-othertable .refrenamer-collapsible{padding-right:20px} .refrenamer-toggle{width:100%;background-position:center ${right} 4px;background-repeat:no-repeat;background-size:12px 12px;background-color:transparent;border:none;cursor:pointer;z-index:1} .refrenamer-expanded > .refrenamer-toggle{width:20px;${left}:auto} .refrenamer-toggle:hover{background-color:var(--background-color-button-quiet--hover,rgba(0,24,73,0.027))} .refrenamer-othertable .mw-reference-text{overflow:hidden;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical} .refrenamer-othertable .refrenamer-collapsible.refrenamer-expanded > .mw-reference-text{-webkit-line-clamp:unset}`);
		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'],
				modes: ['main', 'mainReset', 'other', 'otherReset']
			},
			{
				action: 'continue',
				flags: ['primary', 'progressive'],
				label: getMsg('continue'),
				modes: ['main', 'mainReset', 'other', 'otherReset']
			},
			{
				action: 'addAll',
				flags: ['progressive'],
				label: getMsg('addAll'),
				modes: ['other', 'otherReset']
			},
			{
				action: 'resetSelection',
				label: getMsg('resetSelection'),
				modes: ['main', 'other']
			}
		];
		RefRenamerDialog.prototype.initialize = function () {
			RefRenamerDialog.parent.prototype.initialize.apply(this, arguments);
			this.index = new OO.ui.IndexLayout({
				autoFocus: false
			}).addTabPanels([
				new OO.ui.TabPanelLayout('main', { label: getMsg('tableCaption') }),
				new OO.ui.TabPanelLayout('other', { label: getMsg('otherTableCaption') })
			]).on('set', () => {
				let mode = this.index.getCurrentTabPanelName();
				if (this.refs.every(ref => ref.active === ref.isAuto)) {
					mode += 'Reset';
				}
				this.actions.setMode(mode);
				this.updateSize();
				this.setupCollapsibles();
			});
			this.warning = new OO.ui.MessageWidget({
				showClose: true,
				type: 'warning'
			}).toggle().connect(this, { close: 'updateSize' });
			this.mainSelect = new OO.ui.MenuTagMultiselectWidget({
				input: { autocomplete: false },
				options: [
					{ 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|linkDomain', 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();
			this.replaceSpaceLayout = new OO.ui.FieldLayout(this.replaceSpaceCheck, {
				align: 'inline',
				label: getMsg('replaceSpace')
			});
			this.replaceSpaceInput = new OO.ui.TextInputWidget({
				autocomplete: false
			}).toggle().connect(this, { toggle: 'updateSize' });
			this.replaceSpaceCheck.connect(this.replaceSpaceInput, { change: 'toggle' });
			this.replaceSpaceLayout.$header.append(this.replaceSpaceInput.$element);
			this.yearCheck = new OO.ui.CheckboxInputWidget();
			this.yearLayout = new OO.ui.FieldLayout(this.yearCheck, {
				align: 'inline',
				label: getMsg('year')
			});
			this.yearFallbackCheck = new OO.ui.CheckboxInputWidget();
			this.yearConvertCheck = new OO.ui.CheckboxInputWidget();
			this.yearConvertLayout = new OO.ui.FieldLayout(this.yearConvertCheck, {
				align: 'inline',
				label: getMsg('yearConvert')
			});
			this.latinIncrementCheck = new OO.ui.CheckboxInputWidget();
			this.yearSubLayout = new OO.ui.FieldsetLayout({
				items: [
					new OO.ui.FieldLayout(this.yearFallbackCheck, {
						align: 'inline',
						label: getMsg('yearFallback')
					}),
					this.yearConvertLayout,
					new OO.ui.FieldLayout(this.latinIncrementCheck, {
						align: 'inline',
						label: getMsg('latinIncrement')
					})
				]
			}).toggle().connect(this, { toggle: 'updateSize' });
			this.yearCheck.connect(this.yearSubLayout, { change: 'toggle' });
			this.yearLayout.$header.append(this.yearSubLayout.$element);
			this.incrementDropdown = new OO.ui.DropdownWidget({
				menu: {
					items: [
						new OO.ui.MenuOptionWidget({ data: [1, false] }),
						new OO.ui.MenuOptionWidget({ data: [2, false] }),
						new OO.ui.MenuOptionWidget({ data: [0, true] }),
						new OO.ui.MenuOptionWidget({ data: [1, true] })
					]
				}
			});
			this.incrementDropdown.updateLabels = () => {
				let example = getMsg('incrementExample');
				let delimiter = this.delimitConditionalCheck.isSelected()
					? ''
					: this.delimiterInput.getValue();
				this.incrementDropdown.getMenu().getItems().forEach(item => {
					let [start, incrementAll] = item.getData();
					let label = getMsg(
						'incrementExamples',
						example + (incrementAll ? delimiter + start : ''),
						example + delimiter + (start + Number(incrementAll))
					);
					item.setLabel(label);
					if (item.isSelected()) {
						this.incrementDropdown.setLabel(label);
					}
				});
			};
			this.delimiterInput = new OO.ui.TextInputWidget({
				autocomplete: false
			}).connect(this.incrementDropdown, { change: 'updateLabels' });
			this.delimitConditionalCheck = new OO.ui.CheckboxInputWidget()
				.connect(this.incrementDropdown, { change: 'updateLabels' });
			this.removeUnreusedCheck = new OO.ui.CheckboxInputWidget();
			this.applyButton = new OO.ui.ButtonInputWidget({
				flags: ['primary', 'progressive'],
				label: getMsg('apply'),
				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: [
					this.warning,
					new OO.ui.FieldLayout(this.mainSelect, {
						align: 'top',
						label: getMsg('main')
					}),
					new OO.ui.FieldsetLayout({
						classes: ['refrenamer-split'],
						items: [
							new OO.ui.FieldLayout(this.lowercaseCheck, {
								align: 'inline',
								label: getMsg('lowercase')
							}),
							new OO.ui.FieldLayout(this.removeDiaCheck, {
								align: 'inline',
								label: getMsg('removeDia')
							}),
							new OO.ui.FieldLayout(this.removePunctCheck, {
								align: 'inline',
								label: getMsg('removePunct')
							}),
							this.replaceSpaceLayout,
							this.yearLayout,
							new OO.ui.FieldLayout(this.incrementDropdown, {
								align: 'top',
								label: getMsg('increment')
							}),
							new OO.ui.FieldLayout(this.delimiterInput, {
								align: 'top',
								label: getMsg('delimiter')
							}),
							new OO.ui.FieldLayout(this.delimitConditionalCheck, {
								align: 'inline',
								label: getMsg('delimitConditional')
							})
						]
					}),
					new OO.ui.FieldLayout(this.removeUnreusedCheck, {
						align: 'inline',
						label: getMsg('removeUnreused')
					}),
					this.applyButton,
					this.resetButton
				]
			});
			this.$tbody = $('<tbody>');
			this.$table = $('<table>').addClass('wikitable refrenamer-maintable').append(
				$('<thead>').append(
					$('<tr>').append(
						$('<th>').text(getMsg('tableName')),
						$('<th>').text(getMsg('tableRef')),
						$('<th>').text(getMsg('tableNewName')),
						$('<th>').text(getMsg('tableAddRemove'))
					)
				),
				this.$tbody
			);
			this.index.getTabPanel('main').$element.append(
				this.form.$element, this.$table
			);
			this.$otherTbody = $('<tbody>');
			this.index.getTabPanel('other').$element.append(
				$('<table>').addClass('wikitable refrenamer-othertable').append(
					$('<thead>').append(
						$('<tr>').append(
							$('<th>').text(getMsg('tableName')),
							$('<th>').text(getMsg('tableRef')),
							$('<th>').text(getMsg('tableAddRemove'))
						)
					),
					this.$otherTbody
				)
			);
			this.$body.append(this.index.$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: true,
				increment: 2,
				incrementAll: false,
				delimiter: '-',
				delimitConditional: false,
				removeUnreused: true
			};
		};
		RefRenamerDialog.prototype.getConfig = function () {
			let incrementData = this.incrementDropdown.getMenu()
				.findSelectedItem().getData();
			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(),
				increment: incrementData[0],
				incrementAll: incrementData[1],
				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);
			let replaceSpace = typeof config.replaceSpace === 'string';
			this.replaceSpaceCheck.setSelected(replaceSpace);
			this.replaceSpaceInput
				.setValue(replaceSpace ? config.replaceSpace : '-');
			this.yearCheck.setSelected(config.year);
			this.yearFallbackCheck.setSelected(config.yearFallback);
			this.yearConvertCheck.setSelected(config.yearConvert);
			this.latinIncrementCheck.setSelected(
				// compatibility
				typeof config.latinIncrement === 'number' || config.latinIncrement
			);
			let incrementMenu = this.incrementDropdown.getMenu();
			let incrementItem = incrementMenu
				.findItemFromData([config.increment, config.incrementAll]);
			if (incrementItem) {
				incrementMenu.selectItem(incrementItem);
			} else {
				incrementMenu.selectItemByData([2, false]);
			}
			this.delimiterInput.setValue(config.delimiter);
			this.delimitConditionalCheck.setSelected(config.delimitConditional);
			this.removeUnreusedCheck.setSelected(config.removeUnreused);
		};
		RefRenamerDialog.prototype.applyConfig = function (refs, forceKeep) {
			refs = (refs || this.refs).filter(ref => ref.active);
			if (!refs.length) return;
			let config = this.getConfig(), withYear = new Set();
			let stack = config.main.flatMap(k => k.split('|'));
			let names = refs.map(ref => {
				if (!forceKeep && ref.keepCheck) {
					ref.keepCheck.setSelected(!config.removeUnreused);
					if (config.removeUnreused) return;
				}
				let exports = {
					name: ref.name,
					props: ref.props,
					getElement: () => ref.$ref.clone()[0]
				};
				mw.hook('refrenamer.rename').fire(exports);
				if (typeof exports.newName === 'string') {
					return exports.newName;
				}
				let s;
				stack.some(k => (s = ref.props[k]));
				if (!s) 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 year = config.year && (
					config.yearConvert && ref.props.yearAscii ||
					ref.props.year ||
					config.yearFallback && (
						config.yearConvert && ref.props.textYearAscii ||
						ref.props.textYear
					)
				);
				if (year) {
					let delimiter = config.delimitConditional && /\P{Nd}$/u.test(s)
						? ''
						: config.delimiter;
					s += delimiter + year;
					withYear.add(ref);
				}
				return s;
			});
			let comps = names.map(s => s && normalize(s));
			let hardComps = new Set();
			this.refs.forEach(ref => {
				if (refs.includes(ref)) return;
				hardComps.add(
					ref.active && normalize(ref.input.getValue()) ||
						ref.normalized
				);
			});
			refs.forEach((ref, i) => {
				let s = names[i];
				if (!s) {
					ref.input.setValue('');
					return;
				}
				let normalized = comps[i];
				let useLatin = config.latinIncrement && withYear.has(ref);
				let needsIncrement = hardComps.has(normalized) ||
					comps.indexOf(normalized) !== i ||
					(useLatin || config.incrementAll) &&
					comps.slice(i + 1).includes(normalized);
				if (needsIncrement) {
					let unsuffixed = s;
					let delimiter = useLatin || (
						config.delimitConditional && /\P{Nd}$/u.test(s)
					) ? '' : config.delimiter;
					let increment = useLatin ? 0 : config.increment;
					do {
						s = unsuffixed + delimiter +
							(useLatin ? toLatin(increment) : increment);
						normalized = normalize(s);
						increment++;
					} while (hardComps.has(normalized) || comps.includes(normalized));
					comps.push(normalized);
				}
				ref.input.setValue(s);
			});
		};
		RefRenamerDialog.prototype.setActive = function (refs, active) {
			refs.forEach(ref => {
				ref.setActive(active === undefined ? ref.isAuto : active);
			});
			this.applyConfig(refs);
			let activeCount = this.refs.filter(ref => ref.active).length;
			let inactiveCount = this.refs.length - activeCount;
			let tabs = this.index.getTabs().getItems();
			tabs[0].$label.attr('data-refrenamer', activeCount);
			tabs[1].setDisabled(!inactiveCount)
				.$label.attr('data-refrenamer', inactiveCount);
			this.$table.toggleClass('refrenamer-hidden', !activeCount);
			if (!activeCount) {
				this.index.setTabPanel('other');
			} else if (!inactiveCount) {
				this.index.setTabPanel('main');
			} else {
				this.index.emit('set');
			}
		};
		RefRenamerDialog.prototype.toggleActive = function (ref) {
			this.setActive([ref], !ref.active);
		};
		RefRenamerDialog.prototype.getSetupProcess = function (data) {
			Object.assign(this, data);
			let warnings = [];
			if (this.templates.size) {
				warnings.push(
					document.createTextNode(getMsg('templatesWarn')),
					$('<ul>').append(
						[...this.templates].map(s => $('<li>').append(
							'{{',
							$('<a>').attr({
								href: mw.Title.newFromText('Template:' + s).getUrl(),
								target: '_blank'
							}).text(s),
							'}}'
						))
					)[0]
				);
			}
			if (this.invalid.size) {
				warnings.push(
					document.createTextNode(getMsg('invalidWarn')),
					$('<ul>').append(
						[...this.invalid].map(s => $('<li>').text(s))
					)[0]
				);
			}
			this.warning.setLabel(warnings.length ? $(warnings) : '')
				.toggle(warnings.length);
			this.setConfig(mw.storage.getObject('refrenamer'));
			this.$tbody.empty();
			this.$otherTbody.empty();
			this.refs.forEach(ref => {
				ref.initProps();
				ref.initRows();
			});
			this.setActive(this.refs);
			let hasNonAsciiYear = this.refs.some(ref => (
				ref.props.yearAscii || ref.props.textYearAscii
			));
			this.yearConvertLayout.toggle(hasNonAsciiYear);
			this.index.getCurrentTabPanel().$element.scrollTop(0);
			return RefRenamerDialog.super.prototype.getSetupProcess.call(this, data).next(function () {
				this.index.emit('set');
			}, this);
		};
		RefRenamerDialog.prototype.setupCollapsibles = mw.util.debounce(function () {
			if (this.index.getCurrentTabPanelName() !== 'other') return;
			let refs = this.refs.filter(ref => !ref.active && ref.$toggle);
			let width = refs[0].refCell.clientWidth;
			refs.reverse().forEach(ref => {
				ref.setupCollapsible(width);
			});
			this.updateSize();
		}, 200);
		RefRenamerDialog.prototype.getActionProcess = function (action) {
			if (action === 'addAll') {
				this.setActive(this.refs, true);
			} else if (action === 'resetSelection') {
				this.setActive(this.refs);
			}
			return RefRenamerDialog.super.prototype.getActionProcess.call(this, action).next(function () {
				if (action !== 'continue') return;
				let subs = {}, integers = [], dupes = [];
				let comps = this.refs.filter(ref => !ref.active).map(ref => ref.normalized);
				let hasNonVe;
				this.refs.forEach(ref => {
					if (!ref.active) return;
					if (ref.keepCheck && !ref.keepCheck.isSelected()) {
						subs[ref.normalized] = null;
						hasNonVe = hasNonVe || !ref.isVe;
						return;
					}
					let newName = ref.input.getValue().replace(/^[\s_]+|[\s_]+$/g, '');
					let normalized = normalize(newName);
					if (!normalized || newName === ref.name && ref.names.size === 1) {
						comps.push(ref.normalized);
					} else if (!/\D/.test(normalized)) {
						integers.push(newName);
					} else if (comps.includes(normalized)) {
						dupes.push(newName);
					} else {
						subs[ref.normalized] = newName;
						hasNonVe = hasNonVe || !ref.isVe;
						comps.push(normalized);
					}
				});
				if (integers.length || dupes.length) {
					return new OO.ui.Error($([
						[integers, 'numericError'],
						[dupes, 'duplicatesError']
					].flatMap(([names, msgKey]) => names.length ? [
						document.createTextNode(getMsg(msgKey)),
						$('<ul>').append(names.map(n => $('<li>').text(n)))[0]
					] : [])), { recoverable: false });
				}
				if (!Object.keys(subs).length) {
					return new OO.ui.Error(
						getMsg('noChangesError'),
						{ recoverable: false }
					);
				}
				this.close();
				let newText = this.wikitext.replace(
					/<(ref)\s+(name|follow)\s*=\s*(?:"\s*([^\n"]+?)\s*"?|'\s*([^\n']+?)\s*'?|([^\s>]+?))(\s*\/?)>/gi,
					(s, tag, attr, name1, name2, name3, slash) => {
						let normalized = normalize(name1 || name2 || name3);
						return subs.hasOwnProperty(normalized)
							? subs[normalized]
								? `<${tag} ${attr}="${subs[normalized]}"${slash}>`
								: `<${tag}>`
							: s;
					}
				);
				let iw = mw.config.get('wgWikiID') === 'enwiki' ? '' : 'w:en:';
				let summary = hasNonVe
					? getMsg('genericSummary', iw + 'User:Nardog/RefRenamer')
					: getMsg(
						'summary',
						iw + 'Wikipedia:VisualEditor/Named references',
						iw + 'User:Nardog/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(Date.parse(timestamp) - elapsed)
								.toISOString().slice(0, -5).replace(/\D/g, '');
							formData.push(['wpStarttime', time]);
						}
						formData.push(
							['wpEdittime', this.editTime],
							['editRevId', this.revId],
							['wpIgnoreBlankSummary', 1],
							['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');
				}
				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 });
		};
		RefRenamerDialog.prototype.getBodyHeight = function () {
			return this.index.$menu[0].scrollHeight +
				this.index.getCurrentTabPanel().$element[0].scrollHeight;
		};
		dialog = new RefRenamerDialog();
		let winMan = new OO.ui.WindowManager();
		winMan.addWindows([dialog]);
		winMan.$element.appendTo(OO.ui.getTeleportTarget());
	};
	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 onToggle = function () {
		let expanded = this.parentElement.classList.toggle('refrenamer-expanded');
		let newAction = expanded ? 'collapse' : 'expand';
		this.className = 'refrenamer-toggle oo-ui-icon-' + newAction;
		this.title = getMsg(newAction);
		dialog.updateSize();
	};
	window.refRenamer();
}());