User:Andrybak/Scripts/Unsigned helper.js
Appearance
< User:Andrybak | Scripts
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.
This code will be executed when previewing this page.
This user script seems to have a documentation page at User:Andrybak/Scripts/Unsigned helper.
/*
* This is a fork of https://en.wikipedia.org/w/index.php?title=User:Anomie/unsignedhelper.js&oldid=1219219971
*/
(function () {
const DEBUG = false;
const LOG_PREFIX = `[Unsigned Helper]:`;
function error(...toLog) {
console.error(LOG_PREFIX, ...toLog);
}
function warn(...toLog) {
console.warn(LOG_PREFIX, ...toLog);
}
function info(...toLog) {
console.info(LOG_PREFIX, ...toLog);
}
function debug(...toLog) {
console.debug(LOG_PREFIX, ...toLog);
}
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
const CONFIG = {
undated: 'Undated', // [[Template:Undated]]
unsigned: 'Unsigned', // [[Template:Unsigned]]
};
if (mw.config.get('wgAction') !== 'edit' && mw.config.get('wgAction') !== 'submit' && document.getElementById("editform") == null) {
return;
}
info('Loading...');
function formatErrorSpan(errorMessage) {
return `<span style="color:maroon;"><b>Error:</b> ${errorMessage}</span>`;
}
/**
* Batch size for {@link LazyRevisionIdsLoader}.
*/
const LAZY_REVISION_LOADING_INTERVAL = 50;
/**
* Lazily loads revision IDs for a page. The loading is done linearly,
* in batches of the size of {@link LAZY_REVISION_LOADING_INTERVAL}.
* This is relatively fast because we are not loading the heavy contents
* of the page, only the metadata.
* Gives zero-indexed access to the revisions. Zeroth revision is the newest revision.
*/
class LazyRevisionIdsLoader {
#pagename;
#indexedRevisionPromises = [];
/**
* We are loading revision IDs per LAZY_REVISION_LOADING_INTERVAL
* Each of requests gives us LAZY_REVISION_LOADING_INTERVAL revision IDs.
*/
#historyIntervalPromises = [];
#api = new mw.Api();
constructor(pagename) {
this.#pagename = pagename;
}
#getLastLoadedInterval(upToIndex) {
debug(`#getLastLoadedInterval(${upToIndex}): `, this.#historyIntervalPromises.length);
let i = 0;
while (this.#historyIntervalPromises[i] != undefined && i <= upToIndex) {
i++;
}
debug(`#getLastLoadedInterval(${upToIndex}) = ${i}`);
return [i, this.#historyIntervalPromises[i - 1]];
}
#createIntervalFromResponse(response) {
if ('missing' in response.query.pages[0]) {
return undefined;
}
const interval = {
rvcontinue: response.continue?.rvcontinue,
revisions: response.query.pages[0].revisions,
};
if (response.batchcomplete) {
// remember that MediaWiki has no more revisions to return
interval.batchcomplete = true;
} else {
interval.batchcomplete = false;
}
return interval;
}
async #loadIntervalsRecursive(startIndex, targetIndex, rvcontinue) {
const logMsgPrefix = `#loadIntervalsRecursive(${startIndex}, ${targetIndex}, '${rvcontinue}')`;
return new Promise(async (resolve, reject) => {
// reference documentation: https://en.wikipedia.org/w/api.php?action=help&modules=query%2Brevisions
const intervalQuery = {
action: 'query',
prop: 'revisions',
rvlimit: LAZY_REVISION_LOADING_INTERVAL,
rvprop: 'ids|user', // no 'content' here; 'user' is just for debugging purposes
rvslots: 'main',
formatversion: 2, // v2 has nicer field names in responses
titles: this.#pagename,
};
if (rvcontinue) {
intervalQuery.rvcontinue = rvcontinue;
}
debug(`${logMsgPrefix} Q =`, intervalQuery);
this.#api.get(intervalQuery).then(async (response) => {
try {
if (DEBUG) {
debug(`${logMsgPrefix} R =`, response);
}
const interval = this.#createIntervalFromResponse(response);
this.#historyIntervalPromises[startIndex] = Promise.resolve(interval);
if (startIndex == targetIndex) {
// we've hit the limit of what we want to load so far
resolve(interval);
return;
}
if (interval.batchcomplete) {
// reached the end of batch loading => cannot ask for one more
// for convenience, fill the rest of the array with undefined
for (let i = startIndex + 1; i <= targetIndex; i++) {
this.#historyIntervalPromises[i] = Promise.resolve(undefined);
}
info(`${logMsgPrefix}: This is the last batch returned by MediaWiki`);
if (targetIndex <= startIndex) {
error(`${logMsgPrefix}: something went very wrong`);
}
resolve(undefined);
return;
}
// .batchcomplete has not been reached, call for one more interval (recursive)
const ignored = await this.#loadIntervalsRecursive(startIndex + 1, targetIndex, interval.rvcontinue);
if (this.#historyIntervalPromises[targetIndex] == undefined) {
resolve(undefined);
return;
}
this.#historyIntervalPromises[targetIndex].then(
result => resolve(result),
rejection => reject(rejection)
);
} catch (e) {
reject('loadIntervalsRecursive: ' + e);
}
}, rejection => {
reject('loadIntervalsRecursive via api: ' + rejection);
});
});
}
async #loadInterval(intervalIndex) {
const [firstNotLoadedIntervalIndex, latestLoadedIntervalPromise] = this.#getLastLoadedInterval(intervalIndex);
if (firstNotLoadedIntervalIndex > intervalIndex) {
return this.#historyIntervalPromises[intervalIndex];
}
if (await latestLoadedIntervalPromise?.then(interval => interval.batchcomplete)) {
// latest request returned the last batch in the batch loading of revisions
return Promise.resolve(undefined);
}
const rvcontinue = await latestLoadedIntervalPromise?.then(interval => interval.rvcontinue);
debug(`#loadInterval(${intervalIndex}): ${firstNotLoadedIntervalIndex}, ${rvcontinue}`);
return this.#loadIntervalsRecursive(firstNotLoadedIntervalIndex, intervalIndex, rvcontinue);
}
#indexToIntervalIndex(index) {
return Math.floor(index / LAZY_REVISION_LOADING_INTERVAL);
}
#indexToIndexInInterval(index) {
return index % LAZY_REVISION_LOADING_INTERVAL;
}
#revisionsToString(revisions) {
if (!revisions) {
return "<undefined revisions>";
}
return Array.from(revisions).map((revision, index) => {
return `[${index}]={revid=${revision.revid} by User:${revision.user}}`
}).join(", ");
}
/**
* @param index zero-based index of a revision to load
*/
async loadRevision(index) {
if (this.#indexedRevisionPromises[index]) {
return this.#indexedRevisionPromises[index];
}
const promise = new Promise(async (resolve, reject) => {
const intervalIndex = this.#indexToIntervalIndex(index);
debug(`loadRevision: loading from interval #${intervalIndex}...`);
try {
const interval = await this.#loadInterval(intervalIndex);
if (DEBUG) {
debug(`loadRevision: loaded the interval#${intervalIndex} with revisions: (length=${interval?.revisions?.length}) ${this.#revisionsToString(interval?.revisions)}`);
}
if (interval == undefined) {
resolve(undefined);
return;
}
const indexInInterval = this.#indexToIndexInInterval(index);
if (DEBUG) {
debug(`loadRevision: from the above interval, looking at [${indexInInterval}]`);
}
const theRevision = interval.revisions[indexInInterval];
debug('loadRevision: loaded revision', index, theRevision);
resolve(theRevision);
} catch (e) {
reject('loadRevision: ' + e);
}
});
this.#indexedRevisionPromises[index] = promise;
return promise;
}
}
/**
* Lazily loads full revisions (full wikitext and metadata) for a page.
* Gives zero-indexed access to the revisions. Zeroth revision is the newest revision.
* Loaded revisions are cached to speed up consecutive requests about the
* same page.
*/
class LazyFullRevisionsLoader {
#pagename;
#revisionsLoader;
#indexedContentPromises = [];
#api = new mw.Api();
constructor(pagename) {
this.#pagename = pagename;
this.#revisionsLoader = new LazyRevisionIdsLoader(pagename);
}
/**
* Returns a {@link Promise} with full revision for given index.
*/
async loadContent(index) {
if (this.#indexedContentPromises[index]) {
return this.#indexedContentPromises[index];
}
const promise = new Promise(async (resolve, reject) => {
try {
const revision = await this.#revisionsLoader.loadRevision(index);
if (revision == undefined) {
// this revision doesn't seem to exist
resolve(undefined);
return;
}
// reference documentation: https://en.wikipedia.org/w/api.php?action=help&modules=query%2Brevisions
const contentQuery = {
action: 'query',
prop: 'revisions',
rvlimit: 1, // load the big wikitext only for the revision
rvprop: 'ids|user|timestamp|tags|parsedcomment|content',
rvslots: 'main',
formatversion: 2, // v2 has nicer field names in responses
titles: this.#pagename,
rvstartid: revision.revid,
};
debug('loadContent: contentQuery = ', contentQuery);
this.#api.get(contentQuery).then(response => {
try {
const theRevision = response.query.pages[0].revisions[0];
resolve(theRevision);
} catch (e) {
// just in case the chain `response.query.pages[0].revisions[0]`
// is broken somehow
error('loadContent:', e);
reject('loadContent:' + e);
}
}, rejection => {
reject('loadContent via api:' + rejection);
});
} catch (e) {
error('loadContent:', e);
reject('loadContent: ' + e);
}
});
this.#indexedContentPromises[index] = promise;
return promise;
}
async loadRevisionId(index) {
return this.#revisionsLoader.loadRevision(index);
}
}
function midPoint(lower, upper) {
return Math.floor(lower + (upper - lower) / 2);
}
/**
* Based on https://en.wikipedia.org/wiki/Module:Exponential_search
*/
async function exponentialSearch(lower, upper, candidateIndex, testFunc) {
if (upper === null && lower === candidateIndex) {
throw new Error(`Wrong arguments for exponentialSearch (${lower}, ${upper}, ${candidateIndex}).`);
}
if (lower === upper && lower === candidateIndex) {
throw new Error("Cannot find it");
}
const progressMessage = `Examining [${lower}, ${upper ? upper : '...'}]. Current candidate: ${candidateIndex}`;
if (await testFunc(candidateIndex, progressMessage)) {
if (candidateIndex + 1 == upper) {
return candidateIndex;
}
lower = candidateIndex;
if (upper) {
candidateIndex = midPoint(lower, upper);
} else {
candidateIndex = candidateIndex * 2;
}
return exponentialSearch(lower, upper, candidateIndex, testFunc);
} else {
upper = candidateIndex;
candidateIndex = midPoint(lower, upper);
return exponentialSearch(lower, upper, candidateIndex, testFunc);
}
}
class PageHistoryContentSearcher {
#pagename;
#contentLoader;
#progressCallback;
constructor(pagename, progressCallback) {
this.#pagename = pagename;
this.#contentLoader = new LazyFullRevisionsLoader(this.#pagename);
this.#progressCallback = progressCallback;
}
setProgressCallback(progressCallback) {
this.#progressCallback = progressCallback;
}
getContentLoader() {
return this.#contentLoader;
}
/**
* Uses an exponential initial search followed by a binary search to find
* a snippet of text, which was added to the page.
*/
async findRevisionWhenTextAdded(text, startIndex) {
info(`findRevisionWhenTextAdded(startIndex=${startIndex}): searching for '${text}'`);
return new Promise(async (resolve, reject) => {
try {
const startRevision = await this.#contentLoader.loadRevisionId(startIndex);
if (startRevision == undefined) {
if (startIndex === 0) {
reject("Cannot find the latest revision. Does this page exist?");
} else {
reject(`Cannot find the start revision (index=${startIndex}).`);
}
return;
}
if (startIndex === 0) {
const latestFullRevision = await this.#contentLoader.loadContent(startIndex);
if (!latestFullRevision.slots.main.content.includes(text)) {
reject("Cannot find text in the latest revision. Did you edit it?");
return;
}
}
const foundIndex = await exponentialSearch(startIndex, null, startIndex + 10, async (candidateIndex, progressInfo) => {
try {
this.#progressCallback(progressInfo);
const candidateFullRevision = await this.#contentLoader.loadContent(candidateIndex);
if (candidateFullRevision?.slots?.main?.content == undefined) {
/*
* TODO can we distinguish between
* - `candidateIndex` is out of bounds of the history
* vs
* - `candidateIndex` is obscured from current user ([[WP:REVDEL]] or [[WP:SUPPRESS]])
* ?
*/
warn('testFunc: Cannot load the content for candidateIndex = ' + candidateIndex);
return undefined;
}
// debug('testFunc: checking text of revision:', candidateFullRevision, candidateFullRevision?.slots, candidateFullRevision?.slots?.main);
return candidateFullRevision.slots.main.content.includes(text);
} catch (e) {
reject('testFunc: ' + e);
}
});
if (foundIndex === undefined) {
reject("Cannot find this text.");
return;
}
const foundFullRevision = await this.#contentLoader.loadContent(foundIndex);
resolve({
fullRevision: foundFullRevision,
index: foundIndex,
});
} catch (e) {
reject(e);
}
});
}
}
function isRevisionARevert(fullRevision) {
if (fullRevision.tags.includes('mw-rollback')) {
return true;
}
if (fullRevision.tags.includes('mw-undo')) {
return true;
}
if (fullRevision.parsedcomment.includes('Undid')) {
return true;
}
if (fullRevision.parsedcomment.includes('Reverted')) {
return true;
}
return false;
}
function chooseTemplate(selectedText, fullRevision) {
const user = fullRevision.user;
if (selectedText.includes(`[[User talk:${user}|`) || selectedText.includes(`[[user talk:${user}|`)) {
/*
* assume that presense of something that looks like a wikilink to the user's talk page
* means that the message is just undated, not unsigned
* NB: IP editors have `Special:Contributions` and `User talk` in their signature.
*/
return CONFIG.undated;
}
if (selectedText.includes(`[[User:${user}|`) || selectedText.includes(`[[user:${user}|`)) {
// some ancient undated signatures have only `[[User:` links
return CONFIG.undated;
}
return CONFIG.unsigned;
}
function createTimestampWikitext(timestamp) {
/*
* Format is from https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##time_format_like_in_signatures
*
* The unicode escapes are needed to avoid actual substitution, see
* https://en.wikipedia.org/w/index.php?title=User:Andrybak/Scripts/Unsigned_generator.js&diff=prev&oldid=1229098580
*/
return `\u007B\u007Bsubst:#time:H:i, j xg Y "(UTC)"|${timestamp}}}`;
}
function makeTemplate(user, timestamp, template) {
// <nowiki>
const formattedTimestamp = createTimestampWikitext(timestamp);
if (template == CONFIG.undated) {
return '{{subst:' + template + '|' + formattedTimestamp + '}}';
}
return '{{subst:' + template + '|' + user + '|' + formattedTimestamp + '}}';
// </nowiki>
}
function constructAd() {
return " (using [[w:User:Andrybak/Scripts/Unsigned helper|Unsigned helper]])";
}
function extractDiffIds(template, summary) {
const regex = new RegExp(`mark \\[\\[Template:${template}\\|\\{\\{${template}\\}\\}\\]\\] ((\\[\\[Special:Diff\\\/[0-9]+\\]\\][ ,]*)+)`, 'gm');
let match;
const diffs = new Set();
while ((match = regex.exec(summary)) !== null) {
const diffRegex = /\[\[Special:Diff\/[0-9]+\]\]/gm;
let diffMatch;
while ((diffMatch = diffRegex.exec(match[0])) !== null) {
diffs.add(diffMatch[0]);
}
}
return diffs;
}
function extractAccountDiffIds(summary) {
return extractDiffIds(CONFIG.unsigned, summary);
}
function extractUndatedDiffIds(summary) {
return extractDiffIds(CONFIG.undated, summary);
}
function createEditSummary(template, diffs) {
const diffsStr = Array.from(diffs).join(", ");
if (diffsStr.length === 0) {
return "";
}
return `mark [[Template:${template}|{{${template}}}]] ${diffsStr}`;
}
function createUnsignedEditSummary(diffs) {
return createEditSummary(CONFIG.unsigned, diffs)
}
function createUndatedEditSummary(diffs) {
return createEditSummary(CONFIG.undated, diffs)
}
function regenerateNewSummary(oldText) {
const replaceRegex = /mark (.*) \(using[^)]*\)/gm;
const accountDiffs = extractAccountDiffIds(oldText);
const newAccountText = createUnsignedEditSummary(accountDiffs);
const undatedDiffs = extractUndatedDiffIds(oldText);
const newUndatedText = createUndatedEditSummary(undatedDiffs);
debug('newAccountText', newAccountText);
debug('newUndatedText', newUndatedText);
if (newAccountText.length === 0 && newUndatedText.length === 0) {
return oldText;
} else if (newAccountText.length === 0) {
return oldText.replace(replaceRegex, newUndatedText);
} else if (newUndatedText.length === 0) {
return oldText.replace(replaceRegex, newAccountText);
} else {
return oldText.replace(replaceRegex, newAccountText + ", " + newUndatedText);
}
}
function appendToEditSummary(newSummary) {
const editSummaryField = $("#wpSummary:first");
if (editSummaryField.length === 0) {
warn('Cannot find edit summary text field.');
return;
}
// get text without trailing whitespace
let oldText = editSummaryField.val().trimEnd();
const ad = constructAd();
let newText = "";
debug('oldText', oldText);
if (oldText.match(/[*]\/$/)) {
debug('Section name found');
// check if "/* section name */" is present
newText = oldText + " " + newSummary;
} else if (oldText.length !== 0) {
debug('Some old text found');
newText = oldText.replace(ad, '') + ", " + newSummary;
newText = regenerateNewSummary(newText + ad);
} else {
debug('Old text not found');
newText = newSummary;
}
editSummaryField.val(newText + ad);
}
// kept outside of doAddUnsignedTemplate() to keep all the caches
let searcher;
function getSearcher() {
if (searcher) {
return searcher;
}
const pagename = mw.config.get('wgPageName');
searcher = new PageHistoryContentSearcher(pagename, progressInfo => {
info('Default progress callback', progressInfo);
});
return searcher;
}
async function doAddUnsignedTemplate() {
const form = document.getElementById('editform');
const wikitextEditor = form.elements.wpTextbox1;
/*
* https://doc.wikimedia.org/mediawiki-core/master/js/module-jquery.textSelection.html
* We cannot use wikitextEditor.value here, because this textarea is hidden and
* is not updated with CodeMirror. Therefore, the selection in CodeMirror becomes
* desynced from the text in wikitextEditor.
* However, CodeMirror does respond to textSelection "commands" sent to wikitextEditor.
* The responses correspond with up-to-date wikitext in CodeMirror.
* For reference, see https://en.wikipedia.org/wiki/MediaWiki:Gadget-charinsert-core.js#L-251--L-258
*/
const $editor = $(wikitextEditor);
while ($editor.textSelection('getSelection').endsWith('\n')) {
const [selectionStart, selectionEnd] = $editor.textSelection('getCaretPosition', {startAndEnd:true});
$editor.textSelection('setSelection', {start: selectionStart, end:(selectionEnd - 1)});
}
const originalSelection = $(wikitextEditor).textSelection('getSelection');
let selection = originalSelection;
debug(`doAddUnsignedTemplate: getSelection: '${selection}'`);
selection = selection.replace(new RegExp('[\\s\\S]*\\d\\d:\\d\\d, \\d+ (' + months.join('|') + ') \\d\\d\\d\\d \\(UTC\\)([<]/small[>])?'), '');
selection = selection.replace(/[\s\S]*\n=+.*=+\s*\n/, '');
selection = selection.replace(/^\s+|\s+$/g, '');
debug(`doAddUnsignedTemplate: getSelection filtered: '${selection}'`);
// TODO maybe migrate to https://www.mediawiki.org/wiki/OOUI/Windows/Message_Dialogs
const mainDialog = $('<div>Examining...</div>').dialog({
buttons: {
Cancel: function () {
mainDialog.dialog('close');
}
},
modal: true,
title: 'Adding {{unsigned}}'
});
getSearcher().setProgressCallback(debugInfo => {
/* progressCallback */
info('Showing to user:', debugInfo);
mainDialog.html(debugInfo);
});
function applySearcherResult(searcherResult) {
const fullRevision = searcherResult.fullRevision;
const template = chooseTemplate(selection, fullRevision);
const templateWikitext = makeTemplate(
fullRevision.user,
fullRevision.timestamp,
template
);
// https://doc.wikimedia.org/mediawiki-core/master/js/module-jquery.textSelection.html
$(wikitextEditor).textSelection(
'encapsulateSelection', {
post: " " + templateWikitext
}
);
appendToEditSummary(createEditSummary(template, [`[[Special:Diff/${fullRevision.revid}]]`]));
mainDialog.dialog('close');
}
function reportSearcherResultToUser(searcherResult, dialogTitle, useCb, keepLookingCb, cancelCb, createMainMessageDivFn) {
const fullRevision = searcherResult.fullRevision;
const revid = fullRevision.revid;
const comment = fullRevision.parsedcomment;
const questionDialog = createMainMessageDivFn()
.dialog({
title: dialogTitle,
minWidth: document.body.clientWidth / 5,
modal: true,
buttons: {
"Use that revision": function () {
questionDialog.dialog('close');
useCb();
},
"Keep looking": function () {
questionDialog.dialog('close');
keepLookingCb();
},
"Cancel": function () {
questionDialog.dialog('close');
cancelCb();
},
}
});
}
function reportPossibleRevertToUser(searcherResult, useCb, keepLookingCb, cancelCb) {
const fullRevision = searcherResult.fullRevision;
const revid = fullRevision.revid;
const comment = fullRevision.parsedcomment;
reportSearcherResultToUser(searcherResult, "Possible revert!", useCb, keepLookingCb, cancelCb, () => {
return $('<div>').append(
"The ",
$('<a>').prop({
href: '/w/index.php?diff=prev&oldid=' + revid,
target: '_blank'
}).text(`found revision (index=${searcherResult.index})`),
" may be a revert: ",
comment
);
});
}
function formatTimestamp(timestamp) {
// return new Date(timestamp).toLocaleString();
return timestamp;
}
function revisionByteSize(fullRevision) {
if (fullRevision == null) {
return null;
}
return new Blob([fullRevision.slots.main.content]).size;
}
function formatSizeChange(afterSize, beforeSize) {
const change = afterSize - beforeSize;
const title = `${afterSize} bytes after change of this size`;
const titleAttribute = `title="${title}"`;
let style = '';
let changeText = "" + change;
if (change > 0) {
changeText = "+" + change;
style = 'color: var(--color-content-added,#006400);';
}
if (change < 0) {
// use proper minus sign ([[Plus and minus signs#Minus sign]])
changeText = "−" + Math.abs(change);
style = 'color: var(--color-content-removed,#8b0000);';
}
if (Math.abs(change) > 500) {
// [[Help:Watchlist#How to read a watchlist (or recent changes)]]
style = style + "font-weight:bold;";
}
changeText = `(${changeText})`;
return $('<span>').text(changeText).attr('style', style).attr('title', title);
}
async function reportNormalSearcherResultToUser(searcherResult, useCb, keepLookingCb, cancelCb) {
const fullRevision = searcherResult.fullRevision;
const user = fullRevision.user;
const revid = fullRevision.revid;
const comment = fullRevision.parsedcomment;
const afterSize = revisionByteSize(fullRevision);
const beforeSize = revisionByteSize(await searcher.getContentLoader().loadContent(searcherResult.index + 1));
reportSearcherResultToUser(searcherResult, "Do you want to use this?", useCb, keepLookingCb, cancelCb, () => {
return $('<div>').append(
"Found a revision: ",
$('<a>').prop({
href: '/w/index.php?diff=prev&oldid=' + revid,
target: '_blank'
}).text(`[[Special:Diff/${revid}]] (index=${searcherResult.index})`),
$('<br/>'), '• ', formatTimestamp(fullRevision.timestamp),
$('<br/>'), "• by ", $('<a>').prop({
href: '/wiki/Special:Contributions/' + user.replaceAll(' ', '_'),
target: '_blank'
}).text(`User:${user}`),
$('<br/>'), "• ", formatSizeChange(afterSize, beforeSize),
$('<br/>'), "• edit summary: ", $('<i>').html(comment)
);
});
}
function searchFromIndex(index) {
if (selection == undefined || selection == '') {
mainDialog.html(formatErrorSpan("Please select an unsigned message.") +
" Selected: <code>" + originalSelection + "</code>");
return;
}
searcher.findRevisionWhenTextAdded(selection, index).then(searcherResult => {
if (!mainDialog.dialog('isOpen')) {
// user clicked [cancel]
return;
}
info('Searcher found:', searcherResult);
const useCallback = () => { /* use */
applySearcherResult(searcherResult);
};
const keepLookingCallback = () => { /* keep looking */
// recursive call from a differfent index: `+1` is very important here
searchFromIndex(searcherResult.index + 1);
};
const cancelCallback = () => { /* cancel */
mainDialog.dialog('close');
};
if (isRevisionARevert(searcherResult.fullRevision)) {
reportPossibleRevertToUser(searcherResult, useCallback, keepLookingCallback, cancelCallback);
return;
}
reportNormalSearcherResultToUser(searcherResult, useCallback, keepLookingCallback, cancelCallback);
}, rejection => {
error(`Searcher cannot find requested index=${index}. Got error:`, rejection);
if (!mainDialog.dialog('isOpen')) {
// user clicked [cancel]
return;
}
mainDialog.html(formatErrorSpan(`${rejection}`));
});
}
searchFromIndex(0);
}
window.unsignedHelperAddUnsignedTemplate = function(event) {
event.preventDefault();
event.stopPropagation();
mw.loader.using(['mediawiki.util', 'jquery.ui'], doAddUnsignedTemplate);
return false;
}
if (!window.charinsertCustom) {
window.charinsertCustom = {};
}
if (!window.charinsertCustom.Insert) {
window.charinsertCustom.Insert = '';
}
window.charinsertCustom.Insert += ' {{unsigned}}\x10unsignedHelperAddUnsignedTemplate';
if (!window.charinsertCustom['Wiki markup']) {
window.charinsertCustom['Wiki markup'] = '';
}
window.charinsertCustom['Wiki markup'] += ' {{unsigned}}\x10unsignedHelperAddUnsignedTemplate';
if (window.updateEditTools) {
window.updateEditTools();
}
})();