User:Qwerfjkl/scripts/CFDlister.js

// Fork of User:Writ Keeper/Scripts/autoCloser.js

//

console.log("Test script loaded.");

mw.loader.using(["oojs-ui-core", "oojs-ui-windows", "oojs-ui-widgets"], function() {

function escapeRegexp(string) {

return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string

}

function parseHTML(html) {

// Create a temporary div to parse the HTML

var tempDiv = $('

').html(html);

// Find all li elements

var liElements = tempDiv.find('li');

// Array to store extracted hrefs

var hrefs = [];

let existinghrefRegexp = /^https:\/\/en\.wikipedia.org\/wiki\/(Category:[^?&]+?)$/;

let nonexistinghrefRegexp = /^https:\/\/en\.wikipedia\.org\/w\/index\.php\?title=(Category:[^&?]+?)&action=edit&redlink=1$/;

// Iterate through each li element

liElements.each(function() {

// Find all anchor (a) elements within the current li

let hrefline = [];

var anchorElements = $(this).find('a');

// Extract href attribute from each anchor element

anchorElements.each(function() {

var href = $(this).attr('href');

if (href) {

var existingMatch = existinghrefRegexp.exec(href);

var nonexistingMatch = nonexistinghrefRegexp.exec(href);

if (existingMatch) {

hrefline.push(decodeURIComponent(existingMatch[1]).replaceAll('_', ' '));

}

if (nonexistingMatch) {

hrefline.push(decodeURIComponent(nonexistingMatch[1]).replaceAll('_', ' '));

}

}

});

hrefs.push(hrefline);

});

return hrefs;

}

function handlepaste(widget, e) {

var types, pastedData, parsedData;

// Browsers that support the 'text/html' type in the Clipboard API (Chrome, Firefox 22+)

if (e && e.clipboardData && e.clipboardData.types && e.clipboardData.getData) {

// Check for 'text/html' in types list

types = e.clipboardData.types;

if (((types instanceof DOMStringList) && types.contains("text/html")) ||

($.inArray && $.inArray('text/html', types) !== -1)) {

// Extract data and pass it to callback

pastedData = e.clipboardData.getData('text/html');

parsedData = parseHTML(pastedData);

// Check if it's an empty array

if (!parsedData || parsedData.length === 0) {

// Allow the paste event to propagate for plain text or empty array

return true;

}

let confirmed = confirm('You have pasted formatted text. Do you want this to be converted into wikitext?');

if (!confirmed) return true;

processPaste(widget, pastedData);

// Stop the data from actually being pasted

e.stopPropagation();

e.preventDefault();

return false;

}

}

// Allow the paste event to propagate for plain text

return true;

}

function waitForPastedData(widget, savedContent) {

// If data has been processed by the browser, process it

if (widget.getValue() !== savedContent) {

// Retrieve pasted content via widget's getValue()

var pastedData = widget.getValue();

// Restore saved content

widget.setValue(savedContent);

// Call callback

processPaste(widget, pastedData);

}

// Else wait 20ms and try again

else {

setTimeout(function() {

waitForPastedData(widget, savedContent);

}, 20);

}

}

function processPaste(widget, pastedData) {

// Parse the HTML

var parsedArray = parseHTML(pastedData);

let stringOutput = '';

for (const cats of parsedArray) {

if (cats.length === 1) stringOutput += `* :${cats[0]}\n`;

if (cats.length === 2) stringOutput += `* :${cats[0]} to :${cats[1]}\n`;

if (cats.length === 3) stringOutput += `* :${cats[0]} to :${cats[1]} and :${cats[2]}\n`;

if (cats.length > 3) {

let firstCat = cats.pop(0);

let lastCat = cats.pop(0);

stringOutput += `* :${firstCat}} to :${cats.join(', :')} and :${lastCat}\n`;

}

}

widget.insertContent(stringOutput);

}

// Code from https://doc.wikimedia.org/oojs-ui/master/js/#!/api/OO.ui.Dialog

// Add interface shell

function CfdDialog(config) {

CfdDialog.super.call(this, config);

this.InputText = config.InputText;

this.sectionIndex = config.sectionIndex || null;

CfdDialog.static.title = config.dialogTitle;

CfdDialog.static.actions = config.dialogActions;

}

OO.inheritClass(CfdDialog, OO.ui.ProcessDialog);

CfdDialog.static.name = 'CfdDialog';

CfdDialog.prototype.initialize = function() {

CfdDialog.super.prototype.initialize.call(this);

this.content = new OO.ui.PanelLayout({

padded: false,

expanded: false

});

this.content.$element.append('

Make any changes necessary:

');

CfdDialog.prototype.cfdlisterTextBox = new OO.ui.MultilineTextInputWidget({

autosize: true,

value: this.InputText,

id: "CFD-lister-text",

rows: Math.min((this.InputText.match(/\n/g) || []).length + 2, 10),

maxrows: 25

});

let textInputElement = CfdDialog.prototype.cfdlisterTextBox.$element.get(0);

let handler = handlepaste.bind(this, CfdDialog.prototype.cfdlisterTextBox);

// Modern browsers. Note: 3rd argument is required for Firefox <= 6

if (textInputElement.addEventListener) {

textInputElement.addEventListener('paste', handler, false);

}

// IE <= 8

else {

textInputElement.attachEvent('onpaste', handler);

}

mw.loader.using('ext.wikiEditor', function() {

mw.addWikiEditor(CfdDialog.prototype.cfdlisterTextBox.$input);

});

CfdDialog.prototype.cfdlisterTextBox.$input.on('input', () => this.updateSize());

this.content.$element.append(CfdDialog.prototype.cfdlisterTextBox.$element);

this.$body.append(this.content.$element);

};

CfdDialog.prototype.getActionProcess = function(action) {

var dialog = this;

if (action) {

return new OO.ui.Process(function() {

dialog.close({

text: '\n\n' + CfdDialog.prototype.cfdlisterTextBox.value,

sectionIndex: this.sectionIndex

});

});

}

return CfdDialog.super.prototype.getActionProcess.call(this, action);

};

function editDiscussions() {

var text = localStorage.getItem('CFDNAClist');

if (!text) {

mw.notify('No discussions listed yet.', {

type: 'error'

});

return;

}

var windowManager = new OO.ui.WindowManager();

var cfdDialog = new CfdDialog({

InputText: text.trim(), // newlines will be stripped here and added back implicitly in the closing call

dialogTitle: 'Edit listed discussions',

dialogActions: [{

action: 'add',

label: 'Save',

flags: ['primary', 'progressive']

},

{

label: 'Cancel',

flags: ['destructive', 'safe']

}

]

});

windowManager.defaultSize = 'full';

$(document.body).append(windowManager.$element);

windowManager.addWindows([cfdDialog]);

windowManager.openWindow(cfdDialog);

windowManager.on('closing', (win, closing, data) => {

if (!data) return;

if (!data.text.trim()) {

OO.ui.confirm('Are you sure you want to delete all listed discussions?').done((response) => {

if (response) {

localStorage.setItem('CFDNAClist', '');

localStorage.setItem('CFDNAClist-count', '');

} else mw.notify('Aborted changes to listed discussions.');

});

} else {

localStorage.setItem('CFDNAClist', data.text);

mw.notify('Listed discussions updated.');

}

});

}

var editDiscussionslink = mw.util.addPortletLink('p-cactions', '#', 'Edit discussions', 'pt-cfdnaceditlist', 'Edit listed discussions', null, listDiscussionslink);

$(editDiscussionslink).click(function(event) {

event.preventDefault();

editDiscussions();

});

function quickClose(option, editLink, headerElement) {

if (typeof editLink !== "undefined") {

var regexResults = /title=([^&]+).*§ion=[\D]*(\d+)/.exec(editLink.href);

if (regexResults === null) {

return false;

}

var pageTitle = regexResults[1].replaceAll('_', ' '); // prettify

var sectionIndex = regexResults[2];

const params = {

action: "parse",

format: "json",

page: pageTitle,

prop: "wikitext|sections",

section: sectionIndex

};

const api = new mw.Api();

api.get(params).done(data => {

sectionTitle = data.parse.sections[0].line.replaceAll('_', ' ');

wikitext = data.parse.wikitext["*"];

const closedRegexp = /

if (closedRegexp.test(wikitext)) { // already closed

mw.notify('Discussion already closed, aborted closure.', {

type: 'error'

});

return;

}

const newWikitext = `\n${wikitext.replace(/====.+====/, `$&\n{{subst:cfd top|${option}}}${mw.config.get('wgUserGroups').includes('sysop') ? '' : ' {{subst:nacd}}'}. ~~~~\n`)}\n{{subst:cfd bottom}}`;

var requestData = {

action: "edit",

title: pageTitle,

format: "json",

section: sectionIndex,

text: newWikitext,

summary: `/* ${sectionTitle} */ Quick close as ${option} via script`,

notminor: 1,

nocreate: 1,

token: mw.user.tokens.get('csrfToken')

};

$.ajax({

url: mw.util.wikiScript('api'),

type: 'POST',

dataType: 'json',

data: requestData

})

.then(function(data) {

if (data && data.edit && data.edit.result && data.edit.result == 'Success') {

mw.notify(`Discussion closed as ${option}.`);

// Now use wikitext from before, don't bother refetching

let result = option;

const categoryRegex = /====.+====\s+([\s\S]+)\s+[:\*]* *'''(?:Nominator'?s )?rationale?/i;

if (categoryRegex.test(wikitext)) { // correctly formatted

wikitext = wikitext.match(categoryRegex)[1];

} else {

alert("This nomination is missing a Nominator's rationale: and so the script cannot recognise the categories nominated. Please manually fix this by adding Nominator's rationale: just before the nominator's rationale.");

return;

}

// Cleanup

wikitext = wikitext.replace(/\{\{(?:lc|cl|cls|lcs|clc|cat)\|(.+?)\}\}/gi, ':Category:$1'); // fix category templates

wikitext = wikitext.replaceAll(':Category:Category:', ':Category:'); // fix double category - can be caused by above

wikitext = wikitext.replace(/(propose|delet|renam|split|(?:up)?merg|container).*?/gi, '');

wikitext = wikitext.replace(/^ *[:#\*]* */gm, '* '); // fix indentation

wikitext = wikitext.replace(/^\s*[\*\: ]*\s*\n/gm, ''); // remove lines with just *, : and whitespace

wikitext = wikitext.replace(/\n?\s*\*\s*$/, ''); // remove trailing asterisk

wikitext = wikitext.replace(/
/g, ''); // remove br tags

wikitext = `* ${pageTitle}#${sectionTitle}\nResult: ${result}\n\n; ${pageTitle}\n${wikitext}\n`;

var incorrectOptionRegexp;

switch (option) {

case 'rename':

case 'merge':

incorrectOptionRegexp = /\* *\[\[:Category:[^\]\n]+?\]\]$/gim;

if (incorrectOptionRegexp.test(wikitext)) {

mw.notify('Error parsing nomination. Please click "list discussion" to list it manually.', {

type: 'error'

});

return;

}

break;

case 'delete':

incorrectOptionRegexp = /\* *\[\[:Category:[^\]\n]+?\]\].+\[\[:Category:/gim;

if (incorrectOptionRegexp.test(wikitext)) {

mw.notify('Error parsing nomination. Please click "list discussion" to list it manually.', {

type: 'error'

});

return;

}

break;

default: // shouldn't happen unless the user has modified their html

mw.notify(`Error handling closure: ${option}. Please click "list discussion" to list it manually.`, {

type: 'error'

});

return;

}

if (wikitext.includes('') || wikitext.includes('{{') || wikitext.includes(')? ?(.+?)/i;

if (resultRegexp.test(wikitext)) { // match

result = wikitext.match(resultRegexp)[1];

} else {

result = 'RESULT';

}

wikitext = wikitext.replace(new RegExp(resultRegexp.source + '.+', 'i'), ''); // remove closure text, unneeded

const categoryRegex = /====.+====\s+([\s\S]+)\s+[:\*]* *'''(?:Nominator'?s )?rationale?/i;

if (categoryRegex.test(wikitext)) { // correctly formatted

wikitext = wikitext.match(categoryRegex)[1];

} else {

alert("This nomination is missing a Nominator's rationale: and so the script cannot recognise the categories nominated. Please manually fix this by adding Nominator's rationale: just before the nominator's rationale.");

return;

}

// Cleanup

wikitext = wikitext.replace(/\{\{(?:lc|cl|cls|lcs|clc|cat)\|(.+?)\}\}/gi, ':Category:$1'); // fix category templates

wikitext = wikitext.replaceAll(':Category:Category:', ':Category:'); // fix double category - can be caused by above

wikitext = wikitext.replace(/(propose|delet|renam|split|(?:up)?merg|container).*?/gi, '');

wikitext = wikitext.replace(/^ *[:#\*]* */gm, '* '); // fix indentation

wikitext = wikitext.replace(/^\s*[\*\: ]*\s*\n/gm, ''); // remove lines with just *, : and whitespace

wikitext = wikitext.replace(/\n?\s*\*\s*$/, ''); // remove trailing asterisk

wikitext = wikitext.replace(/
/g, ''); // remove br tags

wikitext = "* " + pageTitle + "#" + sectionTitle + "\nResult: " + result + "\n\n; " + pageTitle + "\n" + wikitext + "\n";

var windowManager = new OO.ui.WindowManager();

var cfdDialog = new CfdDialog({

InputText: wikitext,

sectionIndex: sectionIndex,

dialogTitle: 'List discussion for processing',

dialogActions: [{

action: 'add',

label: 'Add',

flags: ['primary', 'progressive']

},

{

label: 'Cancel',

flags: ['destructive', 'safe']

}

]

});

windowManager.defaultSize = 'full';

$(document.body).append(windowManager.$element);

windowManager.addWindows([cfdDialog]);

windowManager.openWindow(cfdDialog);

windowManager.on('closing', addCFD);

});

}

}

function addCFD(win, closing, data) {

if (data == null || data === undefined || data.text == '' || !data.text) {

return;

}

var wikitext = data.text;

var text = localStorage.getItem('CFDNAClist');

var count = localStorage.getItem('CFDNAClist-count') || 0;

if (text == '' || text == null) {

localStorage.setItem('CFDNAClist', wikitext);

} else {

localStorage.setItem('CFDNAClist', text + wikitext);

}

localStorage.setItem('CFDNAClist-count', Number(count) + 1);

mw.notify('Added discussion');

// double strike through handled sections

if (data.sectionIndex) {

// apply styles to show discussion has been closed (like XFDCloser)

$($("h4")[data.sectionIndex - 2]).css({

'text-decoration': 'line-through',

'text-decoration-style': 'double'

});

var startH4 = document.querySelectorAll("h4")[data.sectionIndex - 2].parentElement;

if (startH4) {

var elementsBetweenH4 = [];

var currentElement = startH4.nextElementSibling;

while (currentElement) {

if (currentElement.classList.contains("mw-heading") && currentElement.classList.contains("mw-heading4")) {

break;

}

elementsBetweenH4.push(currentElement);

currentElement = currentElement.nextElementSibling;

}

elementsBetweenH4.forEach(function(element) {

$(element).css('opacity', '50%');

});

}

}

}

function discussionListerSetup() {

console.log("Adding links to sections");

function createDropdownLink(text, clickHandler) {

var link = document.createElement("a");

link.href = "#";

link.innerHTML = text;

link.onclick = function(event) {

event.preventDefault();

clickHandler();

};

return link;

}

var sectionHeaders = $("h4 ~ .mw-editsection");

$('.mw-heading, h1, h2, h3, h4, h5, h6').css('overflow', 'visible'); // allow for dropdown, no idea what damage it could do

sectionHeaders.each(function(index, element) {

console.log("Potential sect");

var editLink = $(element).children("a")[0];

if (typeof editLink !== "undefined" && /§ion=[\D]*(\d+)/.exec(editLink.href)) {

console.log("Adding to sect");

$(editLink).addClass("sectionEditLink");

var discussionLister = $("List discussion");

discussionLister.click(listDiscussion);

// Create dropdown elements

var dropdownContainer = $(``);

var dropdownTrigger = $(`One click close`);

var dropdownMenu = $(``);

var actions = ['delete', 'rename', 'merge']; // needs to correspond with switch statement in quickClose()

for (var i = 0; i < actions.length; i++) {

const action = actions[i];

var menuItem = $(`${action}`);

menuItem.click(function() {

quickClose(action, editLink, element);

dropdownMenu.hide();

});

// make red on hover

menuItem.on("mouseenter", function() {

$(this).css('color', 'red');

}).on("mouseleave", function() {

$(this).css('color', 'color: #0645AD');

});

dropdownMenu.append(menuItem);

}

dropdownTrigger.click(function() {

dropdownMenu.toggle();

});

// Append elements to the existing element

dropdownContainer.append(dropdownTrigger, dropdownMenu);

// Close the dropdown if the user clicks outside of it

$(document).click(function(event) {

if (!$(event.target).closest('.dropdown-container').length) {

dropdownMenu.hide();

}

});

let bracket = $(element).find('.mw-editsection-bracket').filter(function() {

return $(this).closest(".arky-span").length === 0; // compatibility with archiver.js

}).last();

$(bracket).before(' | ', discussionLister, " | ", dropdownContainer);

}

});

}

if (mw.config.get("wgPageName").match('Wikipedia:Categories_for_discussion')) $(document).ready(discussionListerSetup);

});

async function listPages() {

var text = localStorage.getItem('CFDNAClist');

var count = localStorage.getItem('CFDNAClist-count');

if (text == '' || text == null) {

mw.notify('No discussions to list, aborting');

return;

}

text = "\n\nPlease can an admin add the following:" + text + "\n~~~~";

const date = new Date();

const monthNames = [

"January", "February", "March", "April", "May", "June",

"July", "August", "September", "October", "November", "December"

];

const day = date.getUTCDate();

const month = monthNames[date.getUTCMonth()];

const year = date.getUTCFullYear();

// Return the formatted string

let current_date = `${day} ${month} ${year}`;

// Check if we need to make a new section

let sectionRequestData = {

"action": "parse",

"format": "json",

"page": "Wikipedia talk:Categories for discussion/Working",

"prop": "sections",

"formatversion": "2"

};

let sectionData;

try {

sectionData = await $.ajax({

url: mw.util.wikiScript('api'),

type: 'POST',

dataType: 'json',

data: sectionRequestData

});

} catch (error) {

console.error('Error occurred:', error);

// Handle the error or rethrow it

}

var requestData = {

action: "edit",

title: "Wikipedia talk:Categories for discussion/Working",

format: "json",

summary: "Add NAC request (" + count + " dicussions listed) via script",

notminor: 1,

nocreate: 1,

redirect: 1,

token: mw.user.tokens.get('csrfToken')

};

if (sectionData.parse.sections.length != 0 && sectionData.parse.sections.slice(-1)[0].line.toLowerCase().startsWith("non-admin closure")) { // no need for a new section

requestData.appendtext = text;

} else { // new section

// helper function

const incrementSectionTitle = (sectTitle) => {

const regex = /(.*?)(\d+)?$/; // Matches the main text and optional numeric suffix

const match = sectTitle.match(regex);

if (!match) return sectTitle; // If no match, return the original sectTitle

const base = match[1].trim(); // Main text without the number

const number = match[2] ? parseInt(match[2], 10) : 1; // Extract or default to 1

return `${base} ${number + 1}`; // Increment the number and return

};

// make sure we don't duplicate titles

var sectionTitle = `Non-admin closure request (${current_date})`;

while (sectionData.parse.sections.some(section => section.line === sectionTitle)) {

sectionTitle = incrementSectionTitle(sectionTitle);

}

requestData.section = "new";

requestData.sectiontitle = sectionTitle;

requestData.text = text.replace(/^\s+/, ''); // rstrip

}

mw.notify('Editing WT:CFDW...', {

tag: 'CFDListerEdit'

});

$.ajax({

url: mw.util.wikiScript('api'),

type: 'POST',

dataType: 'json',

data: requestData

})

.then(function(data) {

if (data && data.edit && data.edit.result && data.edit.result == 'Success') {

mw.notify('Discussions listed.', {

tag: 'CFDListerEdit'

});

localStorage.setItem('CFDNAClist', '');

localStorage.setItem('CFDNAClist-count', 0);

window.location.href = "https://en.wikipedia.org/wiki/Wikipedia_talk:Categories_for_discussion/Working#footer"; // redirect

} else {

alert('The edit query returned an error. =(');

}

})

.catch(function() {

alert('The ajax request failed.');

});

}

var listDiscussionslink = mw.util.addPortletLink('p-cactions', '#', 'List discussions', 'pt-cfdnaclist', 'List closed discussions for admins to handle');

$(listDiscussionslink).click(function(event) {

event.preventDefault();

listPages();

});

//