User:Nardog/RefRenamer-core.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
Documentation for this user script can be added at User:Nardog/RefRenamer-core. |
(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();
}());