User:Qwerfjkl/scripts/CFDlister.js
Appearance
< User:Qwerfjkl | 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 user script seems to have a documentation page at User:Qwerfjkl/scripts/CFDlister. |
// Fork of [[User:Writ Keeper/Scripts/autoCloser.js]]
//<nowiki>
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 = $('<div>').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('<p style="padding-left: 5px">Make any changes necessary:</p>');
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 = /<div class="boilerplate cfd vfd xfd-closed mw-archivedtalk"/i;
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 [[User:Qwerfjkl/scripts/CFDlister.js|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(/<br ?\/?>/g, ''); // remove br tags
wikitext = `* [[${pageTitle}#${sectionTitle}]]\nResult: '''${result}'''\n<syntaxhighlight lang="wikitext">\n; [[${pageTitle}]]\n${wikitext}\n</syntaxhighlight>`;
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('<s>') || wikitext.includes('{{') || wikitext.includes('<!--')) {
// something probably needs manual review
mw.notify('Error parsing nomination. Please click "list discussion" to list it manually.', {
type: 'error'
});
return;
}
addCFD(null, null, {
text: '\n\n' + wikitext,
sectionIndex: sectionIndex
});
} else {
console.error('The edit query returned an error. =(\n\n' + JSON.stringify(data));
mw.notify('Error closing discussion, see console for details', {
type: 'error'
});
}
})
.catch(function(e) {
console.error('The ajax request failed.\n\n' + JSON.stringify(e));
});
});
}
}
function listDiscussion() {
editLink = $(this).siblings("a.sectionEditLink")[0];
if (typeof editLink !== "undefined") {
var regexResults = /title=([^&]+).*§ion=[\D]*(\d+)/.exec(editLink.href);
if (regexResults === null) {
return false;
}
var pageTitle = regexResults[1].replaceAll('_', ' ');
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["*"];
resultRegexp = /<div class="boilerplate cfd vfd xfd-closed(?: mw-archivedtalk)?" style="background(?:-color)?:#bff9fc; margin:0 auto; padding:0 10px 0 10px; border:1px solid #AAAAAA;">\n:''The following is an archived discussion concerning one or more categories\. <span style="color:red"\>'''Please do not modify it\.'''<\/span> Subsequent comments should be made on an appropriate discussion page \(such as the category's \[\[Help:Using talk pages\|talk page\]\] or in a \[\[Wikipedia:Deletion review\|deletion review\]\]\)\. No further edits should be made to this section\.''\n\n:''The result of the discussion was:'' ?(?:<!-- ?Template:Cfd top ?-->)? ?'''(.+?)'''/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(/<br ?\/?>/g, ''); // remove br tags
wikitext = "* [[" + pageTitle + "#" + sectionTitle + "]]\nResult: '''" + result + "'''\n<syntaxhighlight lang=\"wikitext\">\n; [[" + pageTitle + "]]\n" + wikitext + "\n</syntaxhighlight>";
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 = $("<a>List discussion</a>");
discussionLister.click(listDiscussion);
// Create dropdown elements
var dropdownContainer = $(`<span class='dropdown-container' style="position:relative; display:inline-block; "></span>`);
var dropdownTrigger = $(`<a class='dropdown-trigger' style="color: #0645AD; text-decoration: none; cursor: pointer">One click close</a>`);
var dropdownMenu = $(`<span class='dropdown-menu' style="display: none; position: absolute; background-color: #fff; border: 1px solid #ddd; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); padding: 5px; min-width: 6em; z-index: 1; left: 0px; top: 0.8em;" ></span>`);
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 = $(`<a style="display: block; color: #0645AD; text-decoration: none; padding: 10px; margin-top: 5px; font-size: 150%" class='dropdown-item'>${action}</a>`);
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').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 [[User:Qwerfjkl/scripts/CFDlister.js|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();
});
//</nowiki>