(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();
}());