User:Former User aDB0haVymg/Gadgets/Close-DRV.js
外观
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
/**
* <nowiki>
* Close-DRV.js
* A simple userscript for Chinese Wikipedia
*
* ----
*
* @author User:Classy_Melissa @ zh-two.iwiki.icu
* @licence unlicense
* @version pre-release
*
*/
// wrap in big function to prevent scope leak
"use strict";
function launchCloseDRVTool() {
if (mw.config.get("wgPageName") !== "Wikipedia:存廢覆核請求") {
// ineligible. Silently return.
return;
}
// script configs and constants
const SCRIPT_IDENT = "[[User:Former User aDB0haVymg/Gadgets/Close-DRV.js|Close-DRV.js]]"; // used in summaries
// tuneable constants
// global variables
let apiEndpoint;
const currentPageTitle = mw.config.get("wgPageName");
let rawWikitext;
// SimpleMWAPI Shim library
class SMAPIError extends Error {
constructor(message) {
super(message);
}
}
class SimpleMWApi {
/**
* Constructs a new instance of SimpleMWApi
* Remember: the MediaWiki core mw.Api is already defined
*/
constructor() {
this.apiInterface = new mw.Api({ajax: {headers: {'Api-User-Agent': 'w:zh:User:ClassyMelissa/Gadgets/Close-DRV.js'}}});
}
/**
* Gets the unparsed content of a given page
*
* @param {String} title the title of the page to request
* @returns {String} a string with the complete content of the title
*
* @throws ReferenceError if there is any API error (e.g. page does not exist)
*
*/
async readPage(title) {
// construct the object
const reqParams = {
"action": "parse",
"format": "json",
"page": title,
"prop": "wikitext"
};
// send the request
const response = (await this.apiInterface.get(reqParams)).parse;
// set a trap to detect any error
if (response.error) {
// there is an error!
throw new SMAPIError(response.error.toString());
}
// there is no error.
// read the text out
return response.wikitext['*'];
}
/**
* Writes wikitext content to a page. Will overwrite existing data.
* If the page doesn't exist yet, this will create the page.
*
* @param {String} title the title of the page to write.
* @param {String} content the wikitext content to write
* @param {String} summary the edit summary. optional.
*
* @throws {SMAPIError} if anything goes wrong
* @returns {Number} the new revid
*
*/
async writePage(title, content, summary = "") {
const reqParams = {
"action": "edit",
"format": "json",
"title": title,
"text": content,
"summary": summary,
};
const response = (await this.apiInterface.postWithEditToken(reqParams)).edit;
// detect error
if (response.result.toLowerCase() === "success") {
return response.newrevid;
} else {
throw new SMAPIError("Edit failed.");
}
}
}
// re-initialise the global apiEndpoint
apiEndpoint = new SimpleMWApi();// UI Controllers
// QA Exploratory Testing Passed 2020-07-16
/**
* Opens a jQuery dialog to perform the function
* @param title
* @param indexOfSection
*/
function openJQDialog(title, indexOfSection) {
const element = getDialogUIElement();
$(element).dialog({
title: `正在關閉:${title}`,
minWidth: 600,
minHeight: 300,
buttons: [
{
text: '確定',
click: () => openButtonOnClick(element, title, indexOfSection) // fancy bind
},
{
text: '取消',
click: function () {
$(this).dialog('close');
}
}
]
});
}
/**
* Generate the DOM element of what's supposed to be inside the dialog.
* @param title {string} the title of the section to close
* @returns {HTMLDivElement}
*/
function getDialogUIElement(title) {
const divElement = document.createElement("div");
divElement.id = "closeDRV-dialog-div";
// ⚠ XSS Hot Spot
divElement.innerHTML = `
<br />
<strong>將{{status}}模板狀態改為:</strong><br />
<select id="status-dropdown-select">
<option value="done">完成</option>
<option value="not done">未完成</option>
<option value="on hold">等待中</option>
<option value="">(無)</option>
</select><br />
<br />
自訂狀態文字:<br />
<input type="text" id="status-comments" style="width: 100%" />
<br /><hr /><br />
處理結果 wikitext (不需簽名):<br />
<input type="text" id="outcome-wikitext" style="width: 100%" />
<br /><hr /><br />
執行操作: <br />
<label><input type="radio" name="next-step" id="next-step-noop" checked />什麼都不做</label> <br />
<label><input type="radio" name="next-step" id="next-step-undelete-all" />還原頁面所有版本</label><br />
<label><input type="radio" name="next-step" id="next-step-open-special" />開啟Special:Undelete以執行進一步操作</label><br />
`;
return divElement;
}
/**
* A simple handler function
* @param divSection
* @param title
* @param indexOfSelection
* @returns {Promise<void>}
*/
function openButtonOnClick(divSection, title, indexOfSelection) {
// shorthand: safe querySelector on divSection
const $ef = (selector) => {
const result = divSection.querySelector(selector);
if (!result) {
// missing elements. Probably already finalised
// Just close the dialog.
$(divSection).dialog("close");
}
return result;
};
let newStatus, newStatusComments, outcomeWikitext;
try {
// extract all form variables
newStatus = $ef('#status-dropdown-select').value;
newStatusComments = $ef("#status-comments").value;
outcomeWikitext = $ef("#outcome-wikitext").value;
} catch (e) {
return;
}
// figure out the next step
let nextOp = "noop";
if ($ef("#next-step-undelete-all").checked) {
nextOp = "undelete";
}
if ($ef("#next-step-open-special").checked) {
nextOp = "special";
}
return performCloseAction(divSection, title, indexOfSelection,
newStatus, newStatusComments, outcomeWikitext, nextOp);
}// Driver
/**
* An abstract wrapper function for performing user requested action.
* @param dialogElement {HTMLDivElement}
* @param titleToClose {string}
* @param indexOfSelection {number}
* @param newStatus {string}
* @param newStatusComments {string}
* @param outcomeWikitext {string}
* @param nextOp {string}
* @returns {Promise<void>}
*/
async function performCloseAction(dialogElement, titleToClose, indexOfSelection,
newStatus, newStatusComments, outcomeWikitext, nextOp) {
// clear out the dialog for status report
dialogElement.innerHTML = "";
const $upd = (string) => {
dialogElement.innerText += string;
};
try {
// get the raw wikitext of this page
$upd("取得頁面原始碼... ");
const oldWikitext = await apiEndpoint.readPage(currentPageTitle);
$upd("取得成功\n")
const newWikitext = doReplaceWikitext(oldWikitext, indexOfSelection, newStatus, newStatusComments, outcomeWikitext);
$upd("應用變更... ");
await apiEndpoint.writePage(currentPageTitle, newWikitext, `/* ${titleToClose} */ 關閉請求 (${SCRIPT_IDENT}) `);
$upd("完成. \n")
if (nextOp === "undelete") {
$upd("還原所有版本... ");
await undeleteAllRevisions(titleToClose);
$upd("完成. \n");
}
if (nextOp === "special") {
$upd("正在開啟還原頁面...");
_redirectToSpecialUndelete(titleToClose);
return;
}
$upd("作業完成,即將重新整理。");
location.reload();
} catch (e) {
$upd("\n錯誤 -- " + e.toString());
}
}
// Exploratory QA Passed 2020-07-16
function _redirectToSpecialUndelete(pageTitle) {
const fullTargetTitle = `Special:Undelete/${pageTitle}`;
const relativeURL = mw.config.get("wgArticlePath").replace("$1", fullTargetTitle);
const fullURL = new URL(relativeURL, location);
location.href = fullURL.toString();
}
/**
* Send a request to the API to undelete all revisions of a page.
* @param pageTitle
* @returns {Promise<void>}
*/
async function undeleteAllRevisions(pageTitle) {
const queryParams = {
"action": "undelete",
"format": "json",
"title": pageTitle,
"reason": `存廢覆核還原 (${SCRIPT_IDENT})`,
"utf8": 1
}
const newApiEndpoint = new mw.Api();
await newApiEndpoint.postWithEditToken(queryParams);
}// DOM Helpers
// Unit QA Passed 2020-07-16
/**
* Inserts all "close" buttons onto the DOM
*/
function insertAllCloseButtons() {
const allH2s = _getAllRelevantH2s();
allH2s.forEach(insertOneCloseButton);
}
/**
* Returns a list of all <h2> elements in the document
* @returns {Array}
* @private
*/
function _getAllRelevantH2s() {
let result = Array.from(document.querySelectorAll("#bodyContent h2"));
// remove the TOC, if presenting
result = result.filter((node) => node.id !== "mw-toc-heading");
return result;
}
/**
* Inserts a close button to one specific <h2>
* @param destinationH2Node {Element}
* @param indexOfButton {number} starting from 0, the index of the button
*/
function insertOneCloseButton(destinationH2Node, indexOfButton) {
const newElement = _generateCloseButtonElement(indexOfButton);
destinationH2Node.insertAdjacentElement("beforeend", newElement);
}
/**
* Gets an HTML element of the close button
* @param indexOfButton {number} starting from 0, the index of the button
* @returns {Element}
* @private
*/
function _generateCloseButtonElement(indexOfButton) {
// generate a specific handler function
const handlerFunction = (event) => closeButtonOnClick(event, indexOfButton);
// generate element itself
const newElement = document.createElement("span");
const $ns = newElement.style;
newElement.classList.add("clamel-close-drv-h2-button");
$ns.marginLeft = "1em";
$ns.fontSize = "75%";
$ns.verticalAlign = "middle";
// ⚠ XSS Hot Spot
newElement.innerHTML = "<span class='mw-ui-button mw-ui-destructive'>關閉段落</span>";
newElement.addEventListener("click", handlerFunction);
return newElement;
}
/**
* Extracts the title of an H2 mw-headline element, nonwithstanding the edit button and close button
* @param targetH2 {Element}
* @returns {string} title
*/
function extractH2Title(targetH2) {
cdAssert(targetH2.tagName === "H2");
return targetH2.querySelector(".mw-headline").innerText;
}
// wikitext processors
/**
* Calculates new wikitext from old wikitext, adding the user requested actions
* @param oldWikitext {string}
* @param indexToReplace {number}
* @param newStatus {string}
* @param newStatusComments {string}
* @param outcomeWikitext {string}
* @returns {string} the new wikitext
*/
function doReplaceWikitext(oldWikitext, indexToReplace, newStatus, newStatusComments, outcomeWikitext) {
// split old wikitext into h2 tokens
const splitToken = new RegExp("^==(?!=)", "mgi");
const allSections = _losslessSplit(oldWikitext, splitToken);
// remove the first section, because it is {{/header}}
allSections.shift();
const oldSectionText = allSections[indexToReplace];
const newSectionText = transformSectionWikitext(oldSectionText, newStatus, newStatusComments, outcomeWikitext);
return oldWikitext.replace(oldSectionText, newSectionText);
}
/**
* Constructs a new status template
* @param newStatus
* @param newStatusComments
* @returns {string}
* @private
*/
function _constructStatusWikitext(newStatus, newStatusComments) {
let newStatusTemplate = "{{Status";
if (newStatus) {
newStatusTemplate += `|1=${newStatus}`;
}
if (newStatusComments) {
newStatusTemplate += `|2=${newStatusComments}`;
}
newStatusTemplate += "}}";
return newStatusTemplate;
}
function transformSectionWikitext(oldSectionWikitext, newStatus, newStatusComments, outcomeWikitext) {
let result;
const statusTemplatePattern = /{{status(.+)}}/i;
const outcomeInsertionPoint = /\*處理結果:<!-- 請勿編輯本行並留待管理員填寫更改 -->/gi;
// sanity check
if (!oldSectionWikitext.match(statusTemplatePattern) || !oldSectionWikitext.match(outcomeInsertionPoint)) {
throw new Error("此章節似乎未使用標準模板格式,或者已經被關閉了;請手動操作。");
}
// newStatus + newStatusComments
const newStatusTemplate = _constructStatusWikitext(newStatus, newStatusComments);
result = oldSectionWikitext.replace(statusTemplatePattern, newStatusTemplate);
// outcomeWikitext
result = result.replace(outcomeInsertionPoint, `*處理結果:${outcomeWikitext} --~~~~`);
return result;
}
// Exploratory QA passed 2020-07-16
function _losslessSplit(string, splitter) {
// insert "SPLITTOKEN" before all splitters
const splitToken = "98764burcgbckgbrcxeroqcdonsathbsn";
const stringWithSplitToken = string.replace(splitter, match => `${splitToken}${match}`);
return stringWithSplitToken.split(splitToken);
}// Other main app files
async function ignite() {
// store the raw wikitext
rawWikitext = await apiEndpoint.readPage(currentPageTitle);
// insert (close) buttons on all HTML H2s
insertAllCloseButtons();
}
/**
* Close button on click handler function
* NOTE - must be bound to a specific indexOfButton value
* @param event {Event} default dom event
* @param indexOfButton {number} starting from 0, the index of the button clicked
*/
function closeButtonOnClick(event, indexOfButton) {
// extract the title of the target h2
const title = extractH2Title(event.target.parentElement.parentElement);
mw.loader.using(['jquery.ui'], openJQDialog.bind(null, title, indexOfButton));
}
function handleError(error) {
}
function cdAssert(conditional) {
if (!conditional) {
throw new Error("Internal error");
}
}
return ignite();
}
launchCloseDRVTool();
// </nowiki>