User:Bosco/Unsigned helper.js: Difference between revisions

From Test Wiki
Content deleted Content added
findRevisionWhenTextAdded: add warning log message
mNo edit summary
 
(8 intermediate revisions by 2 users not shown)
Line 4: Line 4:
(function () {
(function () {
const DEBUG = false;
const DEBUG = false;
const LOG_PREFIX = `[Unsigned Helper]:`;
const LOG_PREFIX = `[簽名工具]:`;


function error(...toLog) {
function error(...toLog) {
Line 22: Line 22:
}
}


const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];


const CONFIG = {
const CONFIG = {
undated: 'Undated', // [[Template:Undated]]
undated: 'Undated', // [[Template:Undated]]
unsignedLoggedIn: 'Unsigned', // [[Template:Unsigned]]
unsignedLoggedIn: '未签名', // [[Template:Unsigned]]
unsignedIp: 'Unsigned IP', // [[Template:Unsigned IP]]
unsignedIp: '未签名', // [[Template:Unsigned]]
};
};


if (mw.config.get('wgAction') !== 'edit' && mw.config.get('wgAction') !== 'submit' && document.getElementById("editform") == null) {
if (mw.config.get('wgAction') !== 'edit' && mw.config.get('wgAction') !== 'submit' && document.getElementById("editform") == null) {
info('Not editing a page. Aborting.');
info('未編輯頁面。暫停中。');
return;
return;
}
}


info('Loading...');
info('載入中...');


function formatErrorSpan(errorMessage) {
function formatErrorSpan(errorMessage) {
return `<span style="color:maroon;"><b>Error:</b> ${errorMessage}</span>`;
return `<span style="color:maroon;"><b>錯誤:</b> ${errorMessage}</span>`;
}
}


/**
* Batch size for {@link LazyRevisionIdsLoader}.
*/
const LAZY_REVISION_LOADING_INTERVAL = 50;
const LAZY_REVISION_LOADING_INTERVAL = 50;


/**
/**
* Lazily loads revision IDs for a page.
* 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.
* Gives zero-indexed access to the revisions. Zeroth revision is the newest revision.
*/
*/
Line 217: Line 223:


/**
/**
* Lazily loads full revisions (wikitext, user, revid, tags, edit summary, etc) for a page.
* Lazily loads full revisions (full wikitext and metadata) for a page.
* Gives zero-indexed access to the revisions. Zeroth revision is the newest revision.
* 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 {
class LazyFullRevisionsLoader {
Line 295: Line 303:
async function exponentialSearch(lower, upper, candidateIndex, testFunc) {
async function exponentialSearch(lower, upper, candidateIndex, testFunc) {
if (upper === null && lower === candidateIndex) {
if (upper === null && lower === candidateIndex) {
throw new Error(`Wrong arguments for exponentialSearch (${lower}, ${upper}, ${candidateIndex}).`);
throw new Error(`exponentialSearch 的錯誤參數(${lower}, ${upper}, ${candidateIndex}).`);
}
}
if (lower === upper && lower === candidateIndex) {
if (lower === upper && lower === candidateIndex) {
throw new Error("Cannot find it");
throw new Error("不能找到");
}
}
const progressMessage = `Examining [${lower}, ${upper ? upper : '...'}]. Current candidate: ${candidateIndex}`;
const progressMessage = `Examining [${lower}, ${upper ? upper : '...'}]. Current candidate: ${candidateIndex}`;
Line 334: Line 342:
}
}


async #findMaxIndex() {
getContentLoader() {
return this.#contentLoader;
info('#findMaxIndex: this.#progressCallback = ', this.#progressCallback);
return exponentialSearch(0, null, 1, async (candidateIndex, progressInfo) => {
this.#progressCallback(progressInfo + ' (max search)');
const candidateRevision = await this.#contentLoader.loadRevisionId(candidateIndex);
if (candidateRevision == undefined) {
return false;
}
return true;
});
}
}


/**
* 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) {
async findRevisionWhenTextAdded(text, startIndex) {
info(`findRevisionWhenTextAdded(startIndex=${startIndex}): searching for '${text}'`);
info(`findRevisionWhenTextAdded(startIndex=${startIndex}): searching for '${text}'`);
Line 353: Line 357:
if (startRevision == undefined) {
if (startRevision == undefined) {
if (startIndex === 0) {
if (startIndex === 0) {
reject("Cannot find the latest revision. Does this page exist?");
reject("不能找到最新版本。此頁面存在嗎?");
} else {
} else {
reject(`Cannot find the start revision (index=${startIndex}).`);
reject(`不能尋找開始版本 (版本=${startIndex}).`);
}
}
return;
return;
Line 362: Line 366:
const latestFullRevision = await this.#contentLoader.loadContent(startIndex);
const latestFullRevision = await this.#contentLoader.loadContent(startIndex);
if (!latestFullRevision.slots.main.content.includes(text)) {
if (!latestFullRevision.slots.main.content.includes(text)) {
reject("Cannot find text in the latest revision. Did you edit it?");
reject("不能尋找最新版本的內容。你編輯了嗎?");
return;
return;
}
}
}
}

const maxIndex = (startIndex === 0) ? null : (await this.#findMaxIndex());
const foundIndex = await exponentialSearch(startIndex, null, startIndex + 10, async (candidateIndex, progressInfo) => {
// const maxIndex = (await this.#findMaxIndex());
info('findRevisionWhenTextAdded: maxIndex =', maxIndex);
const foundIndex = await exponentialSearch(startIndex, maxIndex, startIndex + 10, async (candidateIndex, progressInfo) => {
try {
try {
this.#progressCallback(progressInfo);
this.#progressCallback(progressInfo);
const candidateFullRevision = await this.#contentLoader.loadContent(candidateIndex);
const candidateFullRevision = await this.#contentLoader.loadContent(candidateIndex);
if (candidateFullRevision?.slots?.main?.content == undefined) {
if (candidateFullRevision?.slots?.main?.content == undefined) {
/*
warn('testFunc: Cannot load the content for candidateIndex = ' + candidateIndex);
* 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('測試功能:不能載入candidateIndex的內容 = ' + candidateIndex);
return undefined;
return undefined;
}
}
Line 380: Line 389:
return candidateFullRevision.slots.main.content.includes(text);
return candidateFullRevision.slots.main.content.includes(text);
} catch (e) {
} catch (e) {
reject('testFunc: ' + e);
reject('測試內容: ' + e);
}
}
});
});
if (foundIndex === undefined) {
if (foundIndex === undefined) {
reject("Cannot find this text.");
reject("不能尋找這個內容");
return;
return;
}
}
Line 530: Line 539:


// TODO maybe migrate to https://www.mediawiki.org/wiki/OOUI/Windows/Message_Dialogs
// TODO maybe migrate to https://www.mediawiki.org/wiki/OOUI/Windows/Message_Dialogs
const mainDialog = $('<div>Examining...</div>').dialog({
const mainDialog = $('<div>檢查中...</div>').dialog({
buttons: {
buttons: {
Cancel: function () {
Cancel: function () {
Line 571: Line 580:
.dialog({
.dialog({
title: dialogTitle,
title: dialogTitle,
minWidth: document.body.clientWidth / 5,
modal: true,
modal: true,
buttons: {
buttons: {
Line 597: Line 607:
"The ",
"The ",
$('<a>').prop({
$('<a>').prop({
href: '/w/index.php?diff=prev&oldid=' + revid,
href: '/index.php?diff=prev&oldid=' + revid,
target: '_blank'
target: '_blank'
}).text(`found revision (index=${searcherResult.index})`),
}).text(`已尋找版本 (版本=${searcherResult.index})`),
" may be a revert: ",
" may be a revert: ",
comment
comment
Line 606: Line 616:
}
}


function formatTimestamp(timestamp) {
function reportNormalSearcherResultToUser(searcherResult, useCb, keepLookingCb, cancelCb) {
// 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}位元組`;
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 fullRevision = searcherResult.fullRevision;
const user = fullRevision.user;
const revid = fullRevision.revid;
const revid = fullRevision.revid;
const comment = fullRevision.parsedcomment;
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, () => {
reportSearcherResultToUser(searcherResult, "Do you want to use this?", useCb, keepLookingCb, cancelCb, () => {
return $('<div>').append(
return $('<div>').append(
"Found a revision: ",
"Found a revision: ",
$('<a>').prop({
$('<a>').prop({
href: '/w/index.php?diff=prev&oldid=' + revid,
href: '/index.php?diff=prev&oldid=' + revid,
target: '_blank'
target: '_blank'
}).text(`[[Special:Diff/${revid}]] (index=${searcherResult.index})`),
}).text(`[[Special:Diff/${revid}]] (index=${searcherResult.index})`),
$('<br/>'), '• ', formatTimestamp(fullRevision.timestamp),
".",
$('<br/>'),
$('<br/>'), "• by ", $('<a>').prop({
href: '/wiki/Special:Contributions/' + user.replaceAll(' ', '_'),
"Comment: ",
comment
target: '_blank'
}).text(`User:${user}`),
$('<br/>'), "• ", formatSizeChange(afterSize, beforeSize),
$('<br/>'), "• edit summary: ", $('<i>').html(comment)
);
);
});
});
Line 627: Line 678:
function searchFromIndex(index) {
function searchFromIndex(index) {
if (selection == undefined || selection == '') {
if (selection == undefined || selection == '') {
mainDialog.html(formatErrorSpan("Please select an unsigned message.") +
mainDialog.html(formatErrorSpan("請選擇一個未簽名的訊息") +
" Selected: <code>" + originalSelection + "</code>");
" Selected: <code>" + originalSelection + "</code>");
return;
return;
Line 653: Line 704:
reportNormalSearcherResultToUser(searcherResult, useCallback, keepLookingCallback, cancelCallback);
reportNormalSearcherResultToUser(searcherResult, useCallback, keepLookingCallback, cancelCallback);
}, rejection => {
}, rejection => {
error(`Searcher cannot find requested index=${index}. Got error:`, rejection);
error(`搜尋器不能尋找已要求的索引=${index}. Got error:`, rejection);
if (!mainDialog.dialog('isOpen')) {
if (!mainDialog.dialog('isOpen')) {
// user clicked [cancel]
// user clicked [cancel]