Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// <nowiki>
// TFD:
// Cat support (when emptying a cat of templates)
// oldtfdlist for previous TfDs
// notify Twinkle/UV/AWB


// todo: make counter inline, remove progresss and progressElement from editPAge(), more dynamic reatelimit wait.
// counter semi inline; adjust align in createProgressBar()

(function () {

    function capitalise(s) {
        return s[0].toUpperCase() + s.slice(1);
    }

    const NS_TEMPLATE = 10;
    const NS_CATEGORY = 14;
    const NS_MODULE = 828;


    var XFDconfig = {
        "CFD": {
            "title": "Mass CfD",
            "placeholderDiscussionLink": 'Wikipedia:Categories for discussion/Log/2023 July 23#Category:Archaeological cultures by ethnic group',
            "placeholderNominationTitle": 'Archaeological cultures by ethnic group',
            "placeholderRationale": '[[WP:DEFINING|Non-defining]] category.',
            "pageDemoText": "{{subst:Cfd|sectionName}}",
            "titleDemoText": "Category:FirstNominatedCategory|Category:FirstNominatedCategoryTarget1|Category:Target2\nCategory:Foo|Category:Bar\nCategory:Earth",
            "discussionLinkRegex": /^Wikipedia:Categories for discussion\/Log\/\d\d\d\d \w+ \d\d?#(.+)$/,
            "nominationReplacement": [/==== ?NEW NOMINATIONS ?====\s*(?:<!-- ?Please add the newest nominations below this line ?-->)?/, '$&\n\n${nominationText}'],
            "userNotificationTemplate": 'Cfd mass notice',
            "baseDiscussionPage": 'Wikipedia:Categories for discussion/Log/',
            "normaliseFunction": (title) => { return (new mw.Title(title, NS_CATEGORY)).getPrefixedText() },
            "actions": {
                "Delete": {
                    'prepend': '{{subst:Cfd|${sectionName}}}',
                    'action': 'deleting'
                },
                "Rename": {
                    'prepend': '{{subst:Cfr|$1|${sectionName}}}',
                    'action': 'renaming'
                },
                "Merge": {
                    'prepend': '{{subst:Cfm|$1|${sectionName}}}',
                    'action': 'merging'
                },
                "Split": {
                    'prepend': '{{subst:Cfs|$1|$2|${sectionName}}}',
                    'action': 'splitting'
                },
                "Listify": {
                    'prepend': '{{subst:Cfl|$1|${sectionName}}}',
                    'action': 'listifying'
                },
                "Custom": {
                    'prepend': '{{subst:Cfd|type=|${sectionName}}}',
                    'action': ''
                },
            },
            "displayTemplates": [{
                data: 'lc',
                label: 'Category link with extra links – {{lc}}'
            },
            {
                data: 'clc',
                label: 'Category link with count – {{clc}}'
            },
            {
                data: 'cl',
                label: 'Plain category link – {{cl}}'
            }],
        },
        "RFD": {
            "title": "Mass RfD",
            "placeholderDiscussionLink": 'Wikipedia:Redirects for discussion/Log/2024 May 13#Knightfall (comics)',
            "placeholderNominationTitle": 'Knightfall',
            "placeholderRationale": 'No mention of "Knightfall" in the target article.',
            "pageDemoText": "",
            "titleDemoText": "Title1\nTitle2\nTitle2",
            "discussionLinkRegex": /^Wikipedia:Redirects for discussion\/Log\/\d\d\d\d \w+ \d\d?#(.+)$/,
            "nominationReplacement": [/<!-- ?Add new entries directly below this line\. ?-->/, '$&\n${nominationText}\n'],
            "userNotificationTemplate": 'Rfd mass notice',
            "baseDiscussionPage": 'Wikipedia:Redirects for discussion/Log/',
            "normaliseFunction": (title) => { return new mw.Title(title).getPrefixedText() },
            "actions":
            {
                'prepend': '{{subst:rfd|${sectionName}|content=\n${pageText}\n}}'
            },
            "displayTemplate": "{{subst:rfd2|multi=yes|redirect=${pageName}|target=${redirectTarget}}}"
        },
        "TFD": {
            "title": "Mass TfD",
            "placeholderDiscussionLink": 'Wikipedia:Templates for discussion/Log/2025 August 4#Template:COVID-19 pandemic interactive maps/India/India cases',
            "placeholderNominationTitle": 'COVID-19 pandemic interactive maps/India templates',
            "placeholderRationale": 'Unused templates.',
            "pageDemoText": "{{subst:Tfd|sectionName}}",
            "titleDemoText": "Template:Foo\nTemplate:Bar",
            "discussionLinkRegex": /^Wikipedia:Templates for discussion\/Log\/\d\d\d\d \w+ \d\d?#(.+)$/,
            "nominationReplacement": [/=== ?\[\[Wikipedia:Templates for discussion\/Log\/\d\d\d\d \w+ \d\d?\|\w+ \d\d?\]\] ?===\s*(?:<!-- *Add new listings at the top of the list with the following formats for deletion and merging respectively:[\s\S]+?-->)?/, '$&\n${nominationText}'],
            "userNotificationTemplate": 'Tfd mass notice',
            "baseDiscussionPage": 'Wikipedia:Templates for discussion/Log/',
            "normaliseFunction": (title) => {
                let titleObj = new mw.Title(title);
                if (titleObj.getNamespaceId() != 0) {
                    return titleObj.getPrefixedText()
                } else {
                    return new mw.Title(title, NS_TEMPLATE).getPrefixedText(); // assume Template:
                }
            },
            "actions": {
                "Delete": {
                    'prepend': '{{subst:Tfd|heading=${sectionName}}}',
                    'action': 'deleting'
                },
                "Delete (inline notice)": {
                    'prepend': '{{subst:Tfd|type=inline|heading=${sectionName}}}',
                    'action': 'deleting'
                },
                "Delete (tiny notice)": {
                    'prepend': '{{subst:Tfd|type=tiny|heading=${sectionName}}}',
                    'action': 'deleting'
                },
                "Delete (sidebar notice)": {
                    'prepend': '{{subst:Tfd|type=sidebar|heading=${sectionName}}}',
                    'action': 'deleting'
                },
                "Delete (disabled notice)": {
                    'prepend': '{{subst:Tfd|type=disabled|heading=${sectionName}}}',
                    'action': 'deleting'
                },
                "Merge": {
                    'prepend': '{{subst:Tfm|$1|heading=${sectionName}}}}',
                    'action': 'merging'
                },
                "Merge (inline notice)": {
                    'prepend': '{{subst:Tfm|$1|type=inline|heading=${sectionName}}}',
                    'action': 'merging'
                },
                "Merge (tiny notice)": {
                    'prepend': '{{subst:Tfm|$1|type=tiny|heading=${sectionName}}}',
                    'action': 'merging'
                },
                "Merge (sidebar notice)": {
                    'prepend': '{{subst:Tfm|$1|type=sidebar|heading=${sectionName}}}',
                    'action': 'merging'
                },
                "Merge (disabled notice)": {
                    'prepend': '{{subst:Tfm|$1|type=disabled|heading=${sectionName}}}',
                    'action': 'merging'
                },
                "Custom delete": {
                    'prepend': '{{subst:Tfd|type=|heading=${sectionName}}}',
                    'action': ''
                },
                "Custom merge": {
                    'prepend': '{{subst:Tfm|$1|type=|heading=${sectionName}}}',
                    'action': ''
                },
            },
            "displayTemplate": "{{Tfd links|${pageName}${moduleText}}}"
        },
    };
    const match = /Special:Mass(\w+)/.exec(mw.config.get('wgPageName'));
    const XFD = match ? match[1].toUpperCase() : false;
    const config = XFDconfig[XFD];

    function wipePageContent() {
        var bodyContent = $('#bodyContent');
        if (bodyContent) {
            bodyContent.empty();
        }
        var header = $('#firstHeading');
        if (header) {
            header.text(config.title);
        }
        $('title').text(`${config.title} - Wikipedia`);
    }

    function createProgressElement() {
        var progressContainer = new OO.ui.PanelLayout({
            padded: true,
            expanded: false,
            classes: ['sticky-container']
        });
        return progressContainer;
    }

    function makeInfoPopup(info) {
        var infoPopup = new OO.ui.PopupButtonWidget({
            icon: 'info',
            framed: false,
            label: 'More information',
            invisibleLabel: true,
            popup: {
                head: true,
                icon: 'infoFilled',
                label: 'More information',
                $content: $(`<p>${info}</p>`),
                padded: true,
                align: 'force-left',
                autoFlip: false
            }
        });
        return infoPopup;
    }

    function makeCategoryTemplateDropdown(label) {
        var dropdown = new OO.ui.DropdownInputWidget({
            required: true,
            options: config.displayTemplates
        });
        var fieldlayout = new OO.ui.FieldLayout(
            dropdown,
            {
                label,
                align: 'inline',
                classes: ['newnomonly'],
            }
        );
        return { container: fieldlayout, dropdown };
    }

    function createTitleAndInputFieldWithLabel(label, placeholder, classes = []) {
        var input = new OO.ui.TextInputWidget({
            placeholder
        });


        var fieldset = new OO.ui.FieldsetLayout({
            classes
        });

        fieldset.addItems([
            new OO.ui.FieldLayout(input, {
                label
            }),
        ]);

        return {
            container: fieldset,
            inputField: input,
        };
    }
    // Function to create a title and an input field
    function createTitleAndInputField(title, placeholder, info = false) {
        var container = new OO.ui.PanelLayout({
            expanded: false
        });

        var titleLabel = new OO.ui.LabelWidget({
            label: $(`<span>${title}</span>`)
        });

        var infoPopup = makeInfoPopup(info);
        var inputField = new OO.ui.MultilineTextInputWidget({
            placeholder,
            indicator: 'required',
            rows: 10,
            autosize: true
        });
        if (info) container.$element.append(titleLabel.$element, infoPopup.$element, inputField.$element);
        else container.$element.append(titleLabel.$element, inputField.$element);
        return {
            titleLabel,
            inputField,
            container,
            infoPopup,
        };
    }

    // Function to create a title and an input field
    function createTitleAndSingleInputField(title, placeholder) {
        var container = new OO.ui.PanelLayout({
            expanded: false
        });

        var titleLabel = new OO.ui.LabelWidget({
            label: title
        });

        var inputField = new OO.ui.TextInputWidget({
            placeholder,
            indicator: 'required'
        });

        container.$element.append(titleLabel.$element, inputField.$element);

        return {
            titleLabel,
            inputField,
            container
        };
    }

    function createStartButton() {
        var button = new OO.ui.ButtonWidget({
            label: 'Start',
            flags: ['primary', 'progressive']
        });

        return button;
    }

    function createAbortButton() {
        var button = new OO.ui.ButtonWidget({
            label: 'Abort',
            flags: ['primary', 'destructive']
        });

        return button;
    }

    function createRemoveBatchButton() {
        var button = new OO.ui.ButtonWidget({
            label: 'Remove',
            icon: 'close',
            title: 'Remove',
            classes: [
                'remove-batch-button'
            ],
            flags: [
                'destructive'
            ]
        });
        return button;
    }

    function createNominationToggle() {

        var newNomToggle = new OO.ui.ButtonOptionWidget({
            data: 'new',
            label: 'New nomination',
            selected: true
        });
        var oldNomToggle = new OO.ui.ButtonOptionWidget({
            data: 'old',
            label: 'Old nomination',
        });

        var toggle = new OO.ui.ButtonSelectWidget({
            items: [
                newNomToggle,
                oldNomToggle
            ]
        });
        return {
            toggle,
            newNomToggle,
            oldNomToggle,
        };
    }


    function createMessageElement() {
        var messageElement = new OO.ui.MessageWidget({
            type: 'progress',
            inline: true,
            progressType: 'infinite'
        });
        return messageElement;
    }

    function createWarningMessage() {
        var warningMessage = new OO.ui.MessageWidget({
            type: 'warning',
            style: 'background-color: yellow;'
        });
        return warningMessage;
    }

    function createCompletedElement() {
        var messageElement = new OO.ui.MessageWidget({
            type: 'success',
        });
        return messageElement;
    }

    function createDoingElement() {
        var messageElement = new OO.ui.MessageWidget({
            type: 'info',
        });
        return messageElement;
    }

    function createAbortMessage() { // pretty much a duplicate of ratelimitMessage
        var abortMessage = new OO.ui.MessageWidget({
            type: 'warning',
        });
        return abortMessage;
    }

    function createErrorMessage(text) {
        var errorMessage = new OO.ui.MessageWidget({
            type: 'error',
        });
        errorMessage.setLabel(text);
        return errorMessage;
    }

    function createNominationErrorMessage() { // pretty much a duplicate of ratelimitMessage
        return createErrorMessage('Could not detect where to add new nomination.')
    }

    function createFieldset(headingLabel) {
        var fieldset = new OO.ui.FieldsetLayout({
            label: headingLabel,
        });
        return fieldset;
    }

    function createCheckboxWithLabel(label) {
        var checkbox = new OO.ui.CheckboxInputWidget({
            value: 'a',
            selected: true,
            label: "Foo",
            data: "foo"
        });
        var fieldlayout = new OO.ui.FieldLayout(
            checkbox,
            {
                label,
                align: 'inline',
                selected: true
            }
        );
        return {
            fieldlayout,
            checkbox
        };
    }
    function createMenuOptionWidget(data, label) {
        var menuOptionWidget = new OO.ui.MenuOptionWidget({
            data,
            label
        });
        return menuOptionWidget;
    }
    function createActionDropdown() {
        var items = Object.keys(config.actions)
            .map(action => [action, action]) // [label, data]
            .map(action => createMenuOptionWidget(...action));

        var dropdown = new OO.ui.DropdownWidget({
            label: 'Mass action',
            menu: {
                items
            }
        });
        return { dropdown };
    }

    function createMultiOptionButton() {
        var button = new OO.ui.ButtonWidget({
            label: 'Additional action',
            icon: 'add',
            flags: [
                'progressive'
            ]
        });
        return button;
    }

    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    function makeLink(title) {
        return `<a href="/wiki/${title}" target="_blank">${title}</a>`;
    }

    function getDateDifference(date1) {
        const currentDate = new Date();
        // now
        let date2 = `${currentDate.getUTCFullYear()} ${currentDate.toLocaleString('en', { month: 'long', timeZone: 'UTC' })} ${currentDate.getUTCDate()}`

        // Parse the dates
        const parseDate = (dateString) => {
            const [year, month, day] = dateString.split(' ');
            return new Date(`${year}-${month}-${day}`);
        };

        const d1 = parseDate(date1);
        const d2 = parseDate(date2);

        // Calculate the time difference in milliseconds
        const timeDifference = Math.abs(d2 - d1);

        // Convert the time difference from milliseconds to days
        const dayDifference = Math.ceil(timeDifference / (1000 * 60 * 60 * 24));

        return dayDifference;
    }

    function deepCopy(obj) {
        if (obj === null || typeof obj !== 'object') {
            return obj;
        }

        if (obj instanceof OO.ui.Element) {
            return obj;
        }

        if (Array.isArray(obj)) {
            const copy = [];
            for (let i = 0; i < obj.length; i++) {
                copy[i] = deepCopy(obj[i]);
            }
            return copy;
        }

        const copy = {};
        for (const key in obj) {
            if (obj.hasOwnProperty(key)) {
                copy[key] = deepCopy(obj[key]);
            }
        }
        return copy;
    }

    function delinkWikitext(text) {
        // both piped and unpiped wikilinks
        const wikilinkPattern = /\[\[([^\]]+)\|([^\]]+)\]\]|\[\[([^\]]+)\]\]/g;

        return text.replace(wikilinkPattern, (match, p1, p2, p3) => {
            if (p1 && p2) {
                // If there is both link text and target (piped)
                return p2;  // Just return the link text
            } else if (p3) {
                // If there is only link text, without a target (unpiped)
                return p3;
            }
            return match;
        });
    }

    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\/([^?&]+?)$/;
        let nonexistinghrefRegexp = /^https:\/\/en\.wikipedia\.org\/w\/index\.php\?title=([^&?]+?)&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);
                    let page;
                    if (existingMatch) page = new mw.Title(existingMatch[1]);
                    if (nonexistingMatch) page = new mw.Title(nonexistingMatch[1]);
                    if (page && page.getNamespaceId() > -1 && !page.isTalkPage()) {
                        hrefline.push(page.getPrefixedText());
                    }


                }
            });
            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) {
            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 pages of parsedArray) {
            stringOutput += pages.join('|') + '\n';
        }
        widget.insertContent(stringOutput);
    }


    function getWikitext(pageTitle) {
        var api = new mw.Api();

        var requestData = {
            "action": "query",
            "format": "json",
            "prop": "revisions",
            "titles": pageTitle,
            "formatversion": "2",
            "rvprop": "content",
            "rvlimit": "1",
        };
        return api.get(requestData).then(function (data) {
            var pages = data.query.pages;
            return pages[0].revisions[0].content; // Return the wikitext
        }).catch(function (error) {
            console.error('Error fetching wikitext:', error);
        });
    }

    // function to revert edits - this is hacky, and potentially unreliable
    function revertEdits() {
        var revertAllCount = 0;
        var revertElements = $('.massxfdundo');
        if (!revertElements.length) {
            $('#massxfdrevertlink').replaceWith('Reverts done.');
        } else {
            $('#massxfdrevertlink').replaceWith('<span><span id="revertall-text">Reverting...</span> (<span id="revertall-done">0</span> / <span id="revertall-total">' + revertElements.length + '</span> done)</span>');

            revertElements.each(function (index, element) {
                element = $(element); // jQuery-ify
                var title = element.attr('data-title');
                var revid = element.attr('data-revid');
                revertEdit(title, revid)
                    .then(function () {
                        element.text('. Reverted.');
                        revertAllCount++;
                        $('#revertall-done').text(revertAllCount);
                    }).catch(function () {
                        element.html('. Revert failed. <a href="/wiki/Special:Diff/' + revid + '">Click here</a> to view the diff.');
                    });
            }).promise().done(function () {
                $('#revertall-text').text('Reverts done.');
            });
        }
    }

    function revertEdit(title, revid, retry = false) {
        var api = new mw.Api();


        if (retry) {
            sleep(1000);
        }

        var requestData = {
            action: 'edit',
            title,
            undo: revid,
            format: 'json'
        };
        return new Promise(function (resolve, reject) {
            api.postWithEditToken(requestData).then(function (data) {
                if (data.edit && data.edit.result === 'Success') {
                    resolve(true);
                } else {
                    console.error('Error occurred while undoing edit:', data);
                    reject();
                }
            }).catch(function (error) {
                console.error('Error occurred while undoing edit:', error); // handle: editconflict, ratelimit (retry)
                if (error == 'editconflict') {
                    resolve(revertEdit(title, revid, retry = true));
                } else if (error == 'ratelimited') {
                    setTimeout(function () { // wait a minute
                        resolve(revertEdit(title, revid, retry = true));
                    }, 60000);
                } else {
                    reject();
                }
            });
        });
    }

    function getRedirectData(titles) {
        var api = new mw.Api();
        return api.get({
            action: 'query',
            titles,
            redirects: 1,
            format: 'json'
        }).then(function (data) {
            return data.query;
        }).catch(function (error) {
            console.error('Error occurred while fetching page author:', error);
            return false;
        });
    }

    function getUserData(titles) {
        var api = new mw.Api();
        return api.get({
            action: 'query',
            list: 'users',
            ususers: titles,
            usprop: 'blockinfo|groups', // blockinfo - check if indeffed, groups - check if bot
            format: 'json'
        }).then(function (data) {
            return data.query.users;
        }).catch(function (error) {
            console.error('Error occurred while fetching page author:', error);
            return false;
        });
    }

    function getPageAuthor(title) {
        var api = new mw.Api();
        return api.get({
            action: 'query',
            prop: 'revisions',
            titles: title,
            rvprop: 'user',
            rvdir: 'newer', // Sort the revisions in ascending order (oldest first)
            rvlimit: 1,
            format: 'json'
        }).then(function (data) {
            var pages = data.query.pages;
            var pageId = Object.keys(pages)[0];
            var revisions = pages[pageId].revisions;
            if (revisions && revisions.length > 0) {

                return revisions[0].user;
            } else {
                return false;
            }
        }).catch(function (error) {
            console.error('Error occurred while fetching page author:', error);
            return false;
        });
    }


    // Function to create a list of page authors and filter duplicates
    async function createAuthorList(titles) {
        var authorList = [];
        var promises = titles.map(function (title) {
            return getPageAuthor(title);
        });
        try {
            const authors = await Promise.all(promises);
            let queryBatchSize = 50;
            let authorTitles = authors.filter(Boolean).map(author => author.replace(/ /g, '_')); // Replace spaces with underscores, remove false values
            let filteredAuthorList = [];
            for (let i = 0; i < authorTitles.length; i += queryBatchSize) {
                let batch = authorTitles.slice(i, i + queryBatchSize);
                let batchTitles = batch.join('|');

                await getUserData(batchTitles)
                    .then(response => {
                        response.forEach(user => {
                            if (window.debuggingMode) console.log(user);
                            if (user
                                && (!user.blockexpiry || user.blockexpiry !== "infinite" || 'blockpartial' in user)
                                && !user.groups?.includes('bot')
                                && !filteredAuthorList.includes('User talk:' + user.name))
                                filteredAuthorList.push('User talk:' + user.name);
                        });

                    })
                    .catch(error => {
                        console.error("Error querying API:", error);
                    });
            }
            return filteredAuthorList;
        } catch (error_1) {
            console.error('Error occurred while creating author list:', error_1);
            return authorList;
        }
    }

    // Function to create a list of page authors and filter duplicates
    async function createRedirectTargetsList(titles) {
        try {
            let queryBatchSize = 50;
            let redirectTitles = titles.map(title => title.replace(/ /g, '_')); // Replace spaces with underscores
            let redirectTargets = {};
            let nonredirects = [];
            for (let i = 0; i < redirectTitles.length; i += queryBatchSize) {
                let batch = redirectTitles.slice(i, i + queryBatchSize);
                let batchTitles = batch.join('|');

                await getRedirectData(batchTitles)
                    .then(data => {

                        if ('redirects' in data) {
                            data.redirects.forEach(redirect => {
                                redirectTargets[redirect.from] = redirect.tofragment ? redirect.to + "#" + redirect.tofragment : redirect.to;
                            });
                            let redirects = new Set(data.redirects.map(r => r.to));
                            let pages = new Set(Object.values(data.pages).map(p => p.title));
                            nonredirects.push(...[...pages].filter(x => !redirects.has(x)));
                        } else {
                            nonredirects.push(...Object.values(data.pages).map(p => p.title));
                        }

                    })
                    .catch(error => {
                        console.error("Error querying API:", error);
                    });
            }
            return [redirectTargets, nonredirects];
        } catch (error_1) {
            console.error('Error occurred while fetching redirect targets', error_1);
            return [redirectTargets, nonredirects];
        }
    }

    function editPage(options) {
        const localOptions = deepCopy(options);
        localOptions.text = localOptions.textToModify;
        const api = new mw.Api();
        const messageElement = createMessageElement();

        messageElement.setLabel((localOptions.retry)
            ? $('<span>').text('Retrying ').append($(makeLink(localOptions.title)))
            : $('<span>').text('Editing ').append($(makeLink(localOptions.title))));

        localOptions.progressElement.$element.append(messageElement.$element);
        const container = $('.sticky-container');
        container.scrollTop(container.prop("scrollHeight"));

        if (localOptions.retry) {
            sleep(1000);
        }

        const requestData = {
            action: 'edit',
            title: window.debuggingMode ? 'User:Qwerfjkl/sandbox/51' : localOptions.title,
            summary: localOptions.summary,
            format: 'json'
        };

        if (localOptions.type === 'prepend') {
            requestData.nocreate = 1;
            const targets = localOptions.titlesDict[localOptions.title];

            for (let i = 0; i < targets.length; i++) {
                const placeholder = '$' + (i + 1);
                localOptions.text = localOptions.text.replace(placeholder, targets[i]);
            }
            localOptions.text = localOptions.text.replace(/\$\d/g, '');
            requestData.prependtext = localOptions.text.trim() + '\n\n';
        } else if (localOptions.type === 'append') {
            requestData.appendtext = '\n\n' + localOptions.text.trim();
        } else if (localOptions.type === 'text') {
            requestData.text = localOptions.text;
        }

        return new Promise((resolve, reject) => {
            if (window.abortEdits) {
                messageElement.toggle(false);
                resolve();
                return;
            }

            api.postWithEditToken(requestData)
                .then((data) => {
                    if (data.edit && data.edit.result === 'Success') {
                        messageElement.setType('success');
                        messageElement.setLabel($('<span>' + makeLink(localOptions.title) + ' edited successfully</span><span class="massxfdundo" data-revid="' + data.edit.newrevid + '" data-title="' + localOptions.title + '"></span>'));
                        resolve();
                    } else {
                        handleError('Error occurred while editing', data, localOptions, messageElement, resolve, reject);
                    }
                })
                .catch((error) => handleError('Error occurred while editing', error, localOptions, messageElement, resolve, reject));
        });
    }

    function handleError(msg, error, options, messageElement, resolve, reject) {
        messageElement.setType('error');
        messageElement.setLabel($('<span>' + msg + ' ' + makeLink(options.title) + ': ' + error + '</span>'));
        console.error(msg + ' page:', error);

        if (error === 'editconflict') {
            editPage(deepCopy(options)).then(resolve);
        } else if (error === 'ratelimited') {
            options.progress.setDisabled(true);
            handleRateLimitError(options.ratelimitMessage).then(() => {
                options.progress.setDisabled(false);
                editPage(deepCopy(options)).then(resolve);
            });
        } else {
            reject();
        }
    }


    // global scope - needed to syncronise ratelimits
    var massXFDratelimitPromise = null;
    // Function to handle rate limit errors
    function handleRateLimitError(ratelimitMessage) {
        var modify = !(ratelimitMessage.isVisible()); // only do something if the element hasn't already been shown

        if (massXFDratelimitPromise !== null) {
            return massXFDratelimitPromise;
        }

        massXFDratelimitPromise = new Promise(function (resolve) {
            var remainingSeconds = 60;
            var secondsToWait = remainingSeconds * 1000;
            console.log('Rate limit reached. Waiting for ' + remainingSeconds + ' seconds...');

            ratelimitMessage.setType('warning');
            ratelimitMessage.setLabel('Rate limit reached. Waiting for ' + remainingSeconds + ' seconds...');
            ratelimitMessage.toggle(true);

            var countdownInterval = setInterval(function () {
                remainingSeconds--;
                if (modify) {
                    ratelimitMessage.setLabel('Rate limit reached. Waiting for ' + remainingSeconds + ' second' + ((remainingSeconds === 1) ? '' : 's') + '...');
                }

                if (remainingSeconds <= 0 || window.abortEdits) {
                    clearInterval(countdownInterval);
                    massXFDratelimitPromise = null; // reset
                    ratelimitMessage.toggle(false);
                    resolve();
                }
            }, 1000);

            // Use setTimeout to ensure the promise is resolved even if the countdown is not reached
            setTimeout(function () {
                clearInterval(countdownInterval);
                ratelimitMessage.toggle(false);
                massXFDratelimitPromise = null; // reset
                resolve();
            }, secondsToWait);
        });
        return massXFDratelimitPromise;
    }

    // Function to show progress visually
    function createProgressBar(label) {
        var progressBar = new OO.ui.ProgressBarWidget();
        progressBar.setProgress(0);
        var fieldlayout = new OO.ui.FieldLayout(progressBar, {
            label,
            align: 'inline'
        });
        return {
            progressBar,
            fieldlayout
        };
    }


    // Main function to execute the script
    async function runMassXFD() {

        Object.keys(XFDconfig).forEach(function (XfD) {
            mw.util.addPortletLink('p-tb', mw.util.getUrl(`Special:Mass${XfD}`), `Mass ${XfD}`, `pt-mass${XfD.toLowerCase()}`, `Create a mass ${XfD} nomination`);
        });


        if (XFD && config) {
            // Load the required modules
            mw.loader.using('oojs-ui').done(function () {
                wipePageContent();
                if (!window.debuggingMode) { // annoying when reloading for debugging
                    onbeforeunload = function () {
                        return "Closing this tab will cause you to lose all progress.";
                    };
                }
                elementsToDisable = [];
                var bodyContent = $('#bodyContent');

                mw.util.addCSS(`.sticky-container { 
                bottom: 0;
                width: 100%;
                max-height: 600px; 
                overflow-y: auto;
            }`); // should probably be styled directly on the element than via the stylesheet
                var nominationToggleObj = createNominationToggle();
                var nominationToggle = nominationToggleObj.toggle;

                bodyContent.append(nominationToggle.$element);
                elementsToDisable.push(nominationToggle);

                var rationaleObj = createTitleAndInputField('Rationale:', config.placeholderRationale);
                var rationaleContainer = rationaleObj.container;
                var rationaleInputField = rationaleObj.inputField;
                elementsToDisable.push(rationaleInputField);

                var nominationToggleOld = nominationToggleObj.oldNomToggle;
                var nominationToggleNew = nominationToggleObj.newNomToggle;

                var discussionLinkObj = createTitleAndSingleInputField('Discussion link', config.placeholderDiscussionLink);
                var discussionLinkContainer = discussionLinkObj.container;
                var discussionLinkInputField = discussionLinkObj.inputField;
                elementsToDisable.push(discussionLinkInputField);

                var newNomHeaderObj = createTitleAndSingleInputField('Nomination title', config.placeholderNominationTitle);
                var newNomHeaderContainer = newNomHeaderObj.container;
                var newNomHeaderInputField = newNomHeaderObj.inputField;
                elementsToDisable.push(newNomHeaderInputField);

                bodyContent.append(discussionLinkContainer.$element);
                bodyContent.append(newNomHeaderContainer.$element, rationaleContainer.$element);
                function displayElements() {
                    if (nominationToggleOld.isSelected()) {
                        discussionLinkContainer.$element.show();
                        newNomHeaderContainer.$element.hide();
                        rationaleContainer.$element.hide();
                    }
                    else if (nominationToggleNew.isSelected()) {
                        discussionLinkContainer.$element.hide();
                        newNomHeaderContainer.$element.show();
                        rationaleContainer.$element.show();

                    }
                }
                displayElements();
                nominationToggle.on('select', displayElements);




                function createActionNomination(actionsContainer, first = false) {
                    var count = actions.length + 1;
                    let actionNominationTitle = (XFD === 'CFD' || XFD === 'TFD') ? 'Action batch #' + count : '';
                    var container = createFieldset(actionNominationTitle);
                    actionsContainer.append(container.$element);

                    var actionDropdownObj = createActionDropdown();
                    var dropdown = actionDropdownObj.dropdown;

                    elementsToDisable.push(dropdown);
                    dropdown.$element.css('max-width', 'fit-content');
                    let demoText = config.pageDemoText;
                    var prependTextObj = createTitleAndInputField('Text to tag the nominated pages with:', demoText, info = 'A dollar sign <code>$</code> followed by a number, such as <code>$1</code>, will be replaced with a target specified in the title field, or if not target is specified, will be removed.');
                    var prependTextLabel = prependTextObj.titleLabel;
                    var prependTextInfoPopup = prependTextObj.infoPopup;
                    var prependTextInputField = prependTextObj.inputField;


                    elementsToDisable.push(prependTextInputField);
                    var prependTextContainer = new OO.ui.PanelLayout({
                        expanded: false
                    });
                    var actionObj = createTitleAndInputFieldWithLabel('Action', 'renaming', classes = ['newnomonly']);
                    var actionContainer = actionObj.container;
                    var actionInputField = actionObj.inputField;
                    elementsToDisable.push(actionInputField);
                    actionInputField.$element.css('max-width', 'fit-content');
                    if (nominationToggleOld.isSelected() || XFD === 'TFD') actionContainer.$element.hide(); // make invisible until needed
                    prependTextContainer.$element.append(prependTextLabel.$element, prependTextInfoPopup.$element, dropdown.$element, actionContainer.$element, prependTextInputField.$element);

                    nominationToggle.on('select', function () {
                        if (nominationToggleOld.isSelected()) {
                            $('.newnomonly').hide();
                            if (discussionLinkInputField.getValue().trim()) discussionLinkInputField.emit('change');
                        }
                        else if (nominationToggleNew.isSelected()) {
                            if (XFD === 'CFD' || XFD === 'TFD') $('.newnomonly').show();
                            if (newNomHeaderInputField.getValue().trim()) newNomHeaderInputField.emit('change');
                        }
                    });

                    if (nominationToggleOld.isSelected()) {
                        if (discussionLinkInputField.getValue().match(config.discussionLinkRegex)) {
                            sectionName = discussionLinkInputField.getValue().trim().match(config.discussionLinkRegex)[1];
                        }
                    }
                    else if (nominationToggleNew.isSelected()) {
                        sectionName = newNomHeaderInputField.getValue().trim();
                    }

                    // helper function, makes more accurate.
                    function replaceOccurence(str, find, replace) {

                        if (XFD === 'CFD' || XFD === 'TFD') {
                            // last occurence
                            let index = str.lastIndexOf(find);

                            if (index >= 0) {
                                return str.substring(0, index) + replace + str.substring(index + find.length);
                            } else {
                                return str;
                            }
                        } else if (XFD === 'RFD') { // RfD has stuff after it so we can't just replace last occurence
                            if (str.toLowerCase().startsWith('{{subst:rfd|')) {
                                str = str.replace(/\{\{subst:rfd\|/i, '');
                                return '{{subst:rfd|' + str.replace(find, replace);
                            } else {
                                return str.replace(find, replace); // first occurence
                            }
                        }
                    }

                    var sectionName = sectionName || 'sectionName';
                    var oldSectionName = sectionName;

                    if (XFD === 'RFD') {
                        prependTextInputField.setValue(config.actions.prepend.replace('${sectionName}', delinkWikitext(sectionName)));

                        if (discussionLinkInputField.getValue().match(config.discussionLinkRegex)) {
                            let date = discussionLinkInputField.getValue().trim().match(/^Wikipedia:Redirects for discussion\/Log\/(\d\d\d\d \w+ \d\d)?#.+$/)[1];
                            let difference = getDateDifference(date);
                            if (difference !== 0) {
                                prependTextInputField.setValue(config.actions.prepend.replace('{{subst:rfd|${sectionName}|', `{{subst:rfd|${delinkWikitext(sectionName)}|days=${difference}|`));
                            } // else leave as default above
                        }
                    }

                    discussionLinkInputField.on('change', function () {
                        if (discussionLinkInputField.getValue().match(config.discussionLinkRegex)) {
                            oldSectionName = sectionName;
                            sectionName = discussionLinkInputField.getValue().replace(config.discussionLinkRegex, '$1').trim();
                            var text = prependTextInputField.getValue();

                            if (XFD === 'RFD') {
                                const date = discussionLinkInputField.getValue().trim().match(/^Wikipedia:Redirects for discussion\/Log\/(\d\d\d\d \w+ \d\d?)#.+$/)[1];

                                if (/(\| *days *= *)\d+/.test(text)) { // already has days=, update
                                    text = text.replace(/(\| *days *= *)\d+/, '$1' + getDateDifference(date));
                                    text = replaceOccurence(text, delinkWikitext(oldSectionName), delinkWikitext(sectionName));
                                } else {
                                    text = replaceOccurence(text, delinkWikitext(oldSectionName), delinkWikitext(sectionName) + '|days=' + getDateDifference(date));
                                }
                            } else text = replaceOccurence(text, delinkWikitext(oldSectionName), delinkWikitext(sectionName));

                            prependTextInputField.setValue(text);

                        }
                    });

                    newNomHeaderInputField.on('change', function () {
                        if (newNomHeaderInputField.getValue().trim()) {
                            oldSectionName = sectionName;
                            sectionName = newNomHeaderInputField.getValue().trim();
                            var text = prependTextInputField.getValue();
                            text = replaceOccurence(text, delinkWikitext(oldSectionName), delinkWikitext(sectionName));
                            prependTextInputField.setValue(text);
                        }
                    });

                    dropdown.on('labelChange', function () {
                        let actionData = config.actions[dropdown.getLabel()];
                        prependTextInputField.setValue(actionData.prepend.replace('${sectionName}', delinkWikitext(sectionName)));
                        actionInputField.setValue(actionData.action);
                    });




                    var titleListObj = createTitleAndInputField(`List of titles (one per line${XFD === 'CFD' ? ', <code>Category:</code> prefix is optional' : ''})`, config.titleDemoText, info = 'You can specify targets by adding a pipe <code>|</code> and then the target, e.g. <code>Example|Target1|Target2</code>. These targets can be used in the tagging step.');
                    var titleList = titleListObj.container;
                    var titleListInputField = titleListObj.inputField;
                    var titleListInfoPopup = titleListObj.infoPopup;
                    elementsToDisable.push(titleListInputField);
                    let handler = handlepaste.bind(this, titleListInputField);
                    let textInputElement = titleListInputField.$element.get(0);
                    // 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);
                    }


                    titleListObj.inputField.$element.on('paste', handlepaste);

                    if (XFD === 'RFD') {
                        // some XfDs don't need multiple actions, they're just delete. so hide unnecessary elements'
                        actionContainer.$element.hide();
                        dropdown.$element.hide();
                        prependTextInfoPopup.$element.hide(); // both popups give info about targets which aren't relevant here
                        titleListInfoPopup.$element.hide();
                    }


                    if (!first && XFD !== 'CFD') {
                        var removeButton = createRemoveBatchButton();
                        elementsToDisable.push(removeButton);
                        removeButton.on('click', function () {
                            container.$element.remove();
                            // filter based on the container element
                            actions = actions.filter(function (item) {
                                return item.container !== container;
                            });
                            // Reset labels
                            for (i = 0; i < actions.length; i++) {
                                actions[i].container.setLabel('Action batch #' + (i + 1));
                                actions[i].label = 'Action batch #' + (i + 1);
                            }
                        });

                        container.addItems([removeButton, prependTextContainer, titleList]);

                    } else {
                        container.addItems([prependTextContainer, titleList]);
                    }

                    return {
                        titleListInputField,
                        prependTextInputField,
                        label: 'Action batch #' + count,
                        container,
                        actionInputField
                    };
                }
                var actionsContainer = $('<div />');
                bodyContent.append(actionsContainer);
                var actions = [];
                actions.push(createActionNomination(actionsContainer, first = true));

                var checkboxObj = createCheckboxWithLabel('Notify users?');
                var notifyCheckbox = checkboxObj.checkbox;
                elementsToDisable.push(notifyCheckbox);
                var checkboxFieldlayout = checkboxObj.fieldlayout;
                checkboxFieldlayout.$element.css('margin-bottom', '10px');
                bodyContent.append(checkboxFieldlayout.$element);

                var multiOptionButton = createMultiOptionButton();
                elementsToDisable.push(multiOptionButton);
                multiOptionButton.$element.css('margin-bottom', '10px');
                bodyContent.append(multiOptionButton.$element);
                bodyContent.append('<br />');


                multiOptionButton.on('click', () => {
                    actions.push(createActionNomination(actionsContainer));
                });
                if (XFD !== 'CFD' && XFD !== 'TFD') {
                    multiOptionButton.$element.hide();
                }
                if (XFD === 'CFD') {

                    var categoryTemplateDropdownObj = makeCategoryTemplateDropdown('Category template: (used on the discussion page)');
                    categoryTemplateDropdownContainer = categoryTemplateDropdownObj.container;
                    categoryTemplateDropdown = categoryTemplateDropdownObj.dropdown;
                    categoryTemplateDropdown.$element.css(
                        {
                            'display': 'inline-block',
                            'max-width': 'fit-content',
                            'margin-bottom': '10px'
                        }
                    );
                    elementsToDisable.push(categoryTemplateDropdown);
                    if (nominationToggleOld.isSelected()) categoryTemplateDropdownContainer.$element.hide();
                    bodyContent.append(categoryTemplateDropdownContainer.$element);
                }

                var startButton = createStartButton();
                elementsToDisable.push(startButton);
                bodyContent.append(startButton.$element);



                startButton.on('click', async function () {

                    var isOld = nominationToggleOld.isSelected();
                    var isNew = nominationToggleNew.isSelected();
                    // First check elements
                    var error = false;
                    var regex = config.discussionLinkRegex;
                    if (isOld) {
                        if (!(discussionLinkInputField.getValue().trim()) || !regex.test(discussionLinkInputField.getValue().trim())) {
                            discussionLinkInputField.setValidityFlag(false);
                            error = true;
                        } else {
                            discussionLinkInputField.setValidityFlag(true);
                        }
                    } else if (isNew) {
                        if (!(newNomHeaderInputField.getValue().trim())) {
                            newNomHeaderInputField.setValidityFlag(false);
                            error = true;
                        } else {
                            newNomHeaderInputField.setValidityFlag(true);
                        }

                        if (!(rationaleInputField.getValue().trim())) {
                            rationaleInputField.setValidityFlag(false);
                            error = true;
                        } else {
                            rationaleInputField.setValidityFlag(true);
                        }

                    }

                    batches = actions.map(function ({ titleListInputField, prependTextInputField, label, actionInputField }) {
                        if (!(prependTextInputField.getValue().trim()) || (XFD === 'RFD' && !prependTextInputField.getValue().includes('${pageText}'))) {
                            prependTextInputField.setValidityFlag(false);
                            error = true;
                        } else {
                            prependTextInputField.setValidityFlag(true);

                        }

                        if (isNew && (XFD === 'CFD' || XFD === 'TFD')) {
                            if (!(actionInputField.getValue().trim())) {
                                actionInputField.setValidityFlag(false);
                                error = true;
                            } else {
                                actionInputField.setValidityFlag(true);
                            }
                        }

                        if (!(titleListInputField.getValue().trim())) {
                            titleListInputField.setValidityFlag(false);
                            error = true;
                        } else {
                            titleListInputField.setValidityFlag(true);
                        }

                        // Retreive titles, handle dups
                        var titles = {};
                        var titleList = titleListInputField.getValue().split('\n');
                        function normalise(title) {
                            return config.normaliseFunction(title);
                        }
                        let hasModules = false;
                        let hasCSSPages = false;
                        titleList.forEach(function (title) {
                            try {
                                if (title) {
                                    var targets = title.split('|');
                                    var newTitle = targets.shift();

                                    newTitle = normalise(newTitle);
                                    // make sure all titles are in template or module namespaces
                                    if (XFD === 'TFD') {
                                        if (!newTitle.includes('Template:') && !newTitle.includes('Module:')) {
                                            titleListInputField.setValidityFlag(false);
                                            error = true;
                                        }
                                    }
                                    if (!Object.keys(titles).includes(newTitle)) {
                                        // for TfD skip modules, we deal with them elsewhere
                                        if (XFD === "TFD") {
                                            if (newTitle.includes('Module:')) hasModules = true;
                                            else if (newTitle.endsWith('.css')) hasCSSPages = true;
                                            else titles[newTitle] = targets.map(normalise).map(t => t.replace(/^Template:/, ""));
                                            // tfd templates don't use the prefix, and the replacement is safe after normalistation
                                        } else {
                                            titles[newTitle] = targets.map(normalise);
                                        }


                                    }
                                }
                            } catch (e) {
                                console.error(`[MassXFD] Error parsing title "${title}": ${e}`);
                                titleListInputField.setValidityFlag(false);
                                error = true;
                            }
                        });

                        if (!(Object.keys(titles).length) && !hasModules && !hasCSSPages) {
                            titleListInputField.setValidityFlag(false);
                            error = true;
                        }
                        if (Object.keys(titles).length) {
                            return {
                                titles,
                                prependText: prependTextInputField.getValue().trim(),
                                label,
                                actionInputField
                            };
                        } else {
                            return null;
                        }

                    }).filter(function (item) {
                        return item !== null;
                    });

                    // for TFD, we need to remove the Module: titles, and treat them as seperate batches, one for merging, one for deleting
                    if (XFD === 'TFD') {
                        let mergingModules = {};
                        let deletingModules = {};
                        let cssPages = {};
                        actions.forEach(function ({ titleListInputField, prependTextInputField }) {
                            titleListInputField.getValue().split('\n').forEach(title => {
                                let targets = title.split('|');
                                let newTitle = targets.shift();
                                if (newTitle.trim().endsWith('.css')) {
                                    newTitle = config.normaliseFunction(newTitle);
                                    cssPages[newTitle] = targets.map(config.normaliseFunction);
                                }
                                else if ((new mw.Title(newTitle)).getNamespaceId() === NS_MODULE) {

                                    newTitle = config.normaliseFunction(newTitle);
                                    if (prependTextInputField.getValue().trim().match(/\{\{ *subst: *Tfm/i)) {
                                        mergingModules[newTitle] = targets.map(config.normaliseFunction);
                                    } else {
                                        deletingModules[newTitle] = targets.map(config.normaliseFunction);
                                        // this will be the default, even if we can't detect the action
                                    }
                                }
                            });
                        });
                        let sectionName = '';
                        let discussionPageLink = '';
                        if (nominationToggleOld.isSelected()) {
                            if (discussionLinkInputField.getValue().match(config.discussionLinkRegex)) {
                                sectionName = discussionLinkInputField.getValue().trim().match(config.discussionLinkRegex)[1];
                                discussionPageLink = (new mw.Title(discussionLinkInputField.getValue().trim())).getUrl();
                            }
                        }
                        else if (nominationToggleNew.isSelected()) {
                            sectionName = newNomHeaderInputField.getValue().trim();
                            discussionPageLink = (new mw.Title(sectionName)).getUrl();

                            const date = new Date();

                            const year = date.getUTCFullYear();
                            const month = date.toLocaleString('en', { month: 'long', timeZone: 'UTC' });
                            const day = date.getUTCDate();

                            var discussionPage = `${config.baseDiscussionPage}${year} ${month} ${day}`;
                            discussionPageLink = `${discussionPage}#${newNomHeaderInputField.getValue().trim()}`;
                        }
                        if (Object.keys(mergingModules).length) {
                            batches.unshift({
                                titles: Object.fromEntries(
                                    Object.entries(mergingModules).map(
                                        ([title, targets]) => [`${title}/doc`, targets.map(t => t.replace(/^Module:/, ""))]
                                    )
                                ),
                                prependText: `{{subst:Tfm|$1|type=module|page={{subst:BASEPAGENAME}}|heading=${sectionName}}}`,
                                label: "Merging modules",
                                actionInputField: null
                            });
                        }
                        if (Object.keys(deletingModules).length) {
                            batches.unshift({
                                titles: Object.fromEntries(
                                    Object.entries(deletingModules).map(
                                        ([title, targets]) => [`${title}/doc`, targets.map(t => t.replace(/^Module:/, ""))]
                                    )
                                ),
                                prependText: `{{subst:Tfd|type=module|page={{subst:BASEPAGENAME}}|heading=${sectionName}}}`,
                                label: "Deleting modules",
                                actionInputField: null
                            });
                        }
                        if (Object.keys(cssPages).length) {
                            batches.unshift({
                                titles: cssPages, // we don't need to substitute out the prefix because targets will never be used, as the prependText is hardcoded
                                prependText: `/* This template is being discussed in accordance with Wikipedia's deletion policy. Help reach a consensus at its entry: ${discussionPageLink} */\n`,
                                label: "CSS pages",
                                actionInputField: null
                            });
                        }

                    }



                    if (error) {
                        return;
                    }

                    for (let element of elementsToDisable) {
                        element.setDisabled(true);
                    }


                    $('.remove-batch-button').remove();

                    var abortButton = createAbortButton();
                    bodyContent.append(abortButton.$element);
                    window.abortEdits = false; // initialise
                    abortButton.on('click', function () {

                        // Set abortEdits flag to true
                        if (confirm('Are you sure you want to abort?')) {
                            abortButton.setDisabled(true);
                            window.abortEdits = true;
                        }
                    });
                    var allTitles = batches.reduce((allTitles, obj) => {
                        return allTitles.concat(Object.keys(obj.titles));
                    }, []);


                    if (XFD === 'RFD') {
                        let fetchingRedirectsElement = createDoingElement();
                        fetchingRedirectsElement.setLabel('Fetching redirect targets...');
                        fetchingRedirectsElement.$element.css('margin-top', '16px');
                        bodyContent.append(fetchingRedirectsElement.$element);

                        let fetchedRedirectsElement = createCompletedElement();
                        fetchedRedirectsElement.setLabel('Fetched redirect targets');
                        fetchedRedirectsElement.$element.css('margin-top', '16px');

                        var [redirectTargets, nonredirects] = await createRedirectTargetsList(allTitles);
                        if (window.debuggingMode) console.log(`Redirect targets: ${JSON.stringify(redirectTargets)}`);
                        // console.log(Object.values(redirectTargets).map(title => {
                        //     let page = new mw.Title(title)
                        //     return page.getTalkPage().getPrefixedText()
                        // }))
                        // console.log([... new Set(Object.values(redirectTargets).map(title => {
                        //     let page = new mw.Title(title)
                        //     return page.getTalkPage().getPrefixedText()
                        // }))])
                        // window.batches=batches
                        batches[0].titles = Object.keys(batches[0].titles)
                            .filter(x => !nonredirects.includes(x))
                            .reduce((acc, curr) => {
                                acc[curr] = [];
                                return acc;
                            }, {});



                        if (!Object.keys(redirectTargets).length) {
                            var errorMessageElement = createErrorMessage('None of the titles are redirects, aborting.');
                            bodyContent.append(errorMessageElement.$element);
                            return;
                        }
                        if (nonredirects.length) {
                            let nonredirectsWarningMessage = createWarningMessage();
                            nonredirectsWarningMessage.$element.css({ 'max-height': '20em', 'overflow-y': 'auto' }); // normally shouldn't be needed
                            let nonRedirectsHTML = $('<div>').append($('<span>').text('The following pages were ignored because they are not redirects:'));
                            let $listElement = $('<ul>');
                            nonredirects.forEach(item => {
                                const $listItem = $('<li>').html(makeLink(item));
                                $listElement.append($listItem);
                            });
                            nonRedirectsHTML.append($listElement);
                            nonredirectsWarningMessage.setLabel(nonRedirectsHTML);
                            bodyContent.append(nonredirectsWarningMessage.$element);
                        }

                        fetchingRedirectsElement.$element.hide();
                        bodyContent.append(fetchedRedirectsElement.$element);
                    }


                    let fetchingAuthorsElement = createDoingElement();
                    fetchingAuthorsElement.setLabel('Fetching authors...')
                    fetchingAuthorsElement.$element.css('margin-top', '16px');
                    bodyContent.append(fetchingAuthorsElement.$element);

                    let fetchedAuthorsElement = createCompletedElement();
                    fetchedAuthorsElement.setLabel('Fetched authors')
                    fetchedAuthorsElement.$element.css('margin-top', '16px');
                    let authors;
                    if (redirectTargets) {
                        authors = await createAuthorList(Object.keys(redirectTargets));
                    } else {
                        authors = await createAuthorList(allTitles);
                    }

                    fetchingAuthorsElement.$element.hide();
                    bodyContent.append(fetchedAuthorsElement.$element);


                    async function processContent(options) {
                        function getKeyByValue(object, value) {
                            return Object.keys(object).find(key => object[key] === value);
                        }


                        if (!Array.isArray(options.titles)) {
                            options.titlesDict = options.titles; // dictionary is confusingly used for different things for prepend batches and for redirect notify batches
                            options.titles = Object.keys(options.titles);
                        } else {
                            options.titlesDict = {};
                        }

                        const fieldset = createFieldset(options.headingLabel);
                        bodyContent.append(fieldset.$element);

                        options.progressElement = createProgressElement();
                        fieldset.addItems([options.progressElement]);

                        options.ratelimitMessage = createWarningMessage();
                        options.ratelimitMessage.toggle(false);
                        fieldset.addItems([options.ratelimitMessage]);

                        const progressObj = createProgressBar(`(0 / ${options.titles.length}, 0 errors)`);
                        options.progress = progressObj.progressBar;
                        const progressContainer = progressObj.fieldlayout;
                        options.progress.$element.css('margin-top', '5px');
                        options.progress.pushPending();
                        fieldset.addItems([progressContainer]);

                        let resolvedCount = 0;
                        let rejectedCount = 0;

                        function updateCounter() {
                            progressContainer.setLabel(`(${resolvedCount} / ${options.titles.length}, ${rejectedCount} errors)`);
                        }

                        function updateProgress() {
                            const percentage = (resolvedCount + rejectedCount) / options.titles.length * 100;
                            options.progress.setProgress(percentage);
                        }

                        function trackPromise(promise) {
                            return new Promise((resolve) => {
                                promise
                                    .then(value => {
                                        resolvedCount++;
                                        updateCounter();
                                        updateProgress();
                                        resolve(value);
                                    })
                                    .catch(error => {
                                        rejectedCount++;
                                        updateCounter();
                                        updateProgress();
                                        resolve(error);
                                    });
                            });
                        }

                        const promises = [];
                        for (const title of options.titles) {
                            let data = deepCopy(options);
                            if (XFD === 'RFD' && data.type === 'prepend') {
                                const text = await getWikitext(title);
                                data.textToModify = data.textToModify.replace('${pageText}', text);
                                data.type = 'text';
                            }

                            if (data.id === 'rfd-notify-target') {
                                // ${redirectTitle} is a placeholder for the redirect being nominated
                                // this code needs a more intelligent way of checking which redirect was tagged.
                                data.textToModify = data.textToModify.replace('${redirectTitle}', data.titlesDict[title]);
                            }

                            data.title = title;

                            const promise = editPage(data);
                            promises.push(trackPromise(promise));

                            if (!window.abortEdits) await sleep(100); // space out calls - not needed if they're being rejected
                            await massXFDratelimitPromise; // stop if ratelimit reached (global variable)
                        }

                        await Promise.allSettled(promises);

                        options.progress.toggle(false);

                        if (window.abortEdits) {
                            const abortMessage = createAbortMessage();
                            const revertEditsLink = $('<a id="massxfdrevertlink">Revert?</a>');
                            revertEditsLink.on('click', revertEdits);
                            abortMessage.setLabel($('<span>').append('Edits manually aborted. ').append(revertEditsLink));
                            bodyContent.append(abortMessage.$element);
                        } else {
                            const completedElement = createCompletedElement();
                            completedElement.setLabel(options.doneMessage);
                            completedElement.$element.css('margin-bottom', '16px');
                            bodyContent.append(completedElement.$element);
                        }
                    }



                    const date = new Date();

                    const year = date.getUTCFullYear();
                    const month = date.toLocaleString('en', { month: 'long', timeZone: 'UTC' });
                    const day = date.getUTCDate();

                    var summaryDiscussionLink;
                    var discussionPage = `${config.baseDiscussionPage}${year} ${month} ${day}`;

                    if (isOld) summaryDiscussionLink = discussionLinkInputField.getValue().trim();
                    else if (isNew) summaryDiscussionLink = `${discussionPage}#${newNomHeaderInputField.getValue().trim()}`;
                    summaryDiscussionLink = delinkWikitext(summaryDiscussionLink); // links can't be nested

                    const advSummary = ' ([[User:Qwerfjkl/scripts/massXFD.js|via MassXfD.js]])';
                    // WIP, not finished
                    const taggingSummary = 'Tagging page for [[' + summaryDiscussionLink + ']]' + advSummary;
                    const userSummary = 'Notifying user about [[' + summaryDiscussionLink + ']]' + advSummary;
                    const userNotification = `{{ subst: ${config.userNotificationTemplate} | ${summaryDiscussionLink} }} ~~~~`;
                    const nominationSummary = `Adding mass nomination at [[#${newNomHeaderInputField.getValue().trim()}]]` + advSummary;
                    if (XFD === 'RFD') {
                        var redirectTargetNotification = `{{subst:Rfd notice|\${redirectTitle}|${newNomHeaderInputField.getValue().trim()}}} ~~~~`;
                        var redirectTargetNotificationSummary = `Notice of [[${summaryDiscussionLink}]]${advSummary}`;
                    }
                    var batchesToProcess = [];

                    var newNomPromise = new Promise(function (resolve) {
                        if (isNew) {
                            nominationText = `==== ${newNomHeaderInputField.getValue().trim()} ====\n`;
                            for (const batch of batches) {
                                var action = batch.actionInputField?.getValue()?.trim() || false;
                                for (const page of Object.keys(batch.titles)) {
                                    if (XFD == 'CFD') {
                                        var targets = batch.titles[page].slice(); // copy array
                                        var targetText = '';
                                        if (targets.length) {
                                            if (targets.length === 2) {
                                                targetText = ` to [[:${targets[0]}]] and [[:${targets[1]}]]`;
                                            }
                                            else if (targets.length > 2) {
                                                var lastTarget = targets.pop();
                                                targetText = ' to [[:' + targets.join(']], [[:') + ']], and [[:' + lastTarget + ']]';
                                            } else { // 1 target
                                                targetText = ' to [[:' + targets[0] + ']]';
                                            }
                                        }
                                        nominationText += `:* '''Propose ${action}''' {{${categoryTemplateDropdown.getValue()}|${categoryTemplateDropdown.getValue() === 'cl' ? page.replace(/^ *Category:/i, '') : page}}}${targetText}\n`;
                                    } else if (XFD === 'RFD') {
                                        nominationText += config.displayTemplate.replaceAll('${pageName}', page).replaceAll('${redirectTarget}', redirectTargets[page]) + '\n';
                                    } else if (XFD === 'TFD') {
                                        let moduleText = '';
                                        if ((new mw.Title(page)).getNamespacePrefix() === "Module:") moduleText = `|module=Module:`; // looking at the actual implementation, module can be anything that's not empty
                                        nominationText += `* ${config.displayTemplate.replaceAll('${pageName}', (new mw.Title(page)).getMainText()).replaceAll('${moduleText}', moduleText)}\n`;
                                    } else {
                                        throw new Error("Unimplemented.")
                                    }
                                }
                            }
                            var rationale = rationaleInputField.getValue().trim().replace(/\n/, '<br />');
                            nominationText += `${XFD === 'CFD' ? ":'''Nominator's rationale:''' " : ''}${rationale} ~~~~`;
                            if (XFD === 'TFD') nominationText += "\n"; // formatting is inconsistent accross XfDs
                            var newText;

                            getWikitext(discussionPage).then(function (wikitext) {
                                if (!wikitext.match(config.nominationReplacement[0])) {
                                    console.log(wikitext)
                                    console.log(config.nominationReplacement)
                                    var nominationErrorMessage = createNominationErrorMessage();
                                    bodyContent.append(nominationErrorMessage.$element);
                                } else {
                                    newText = wikitext.replace(...config.nominationReplacement).replace('${nominationText}', nominationText);
                                    batchesToProcess.push({
                                        titles: [discussionPage],
                                        textToModify: newText,
                                        summary: nominationSummary,
                                        type: 'text',
                                        doneMessage: 'Nomination added',
                                        headingLabel: 'Creating nomination'
                                    });
                                    resolve();
                                }
                            }).catch(function (error) {
                                console.error('An error occurred in fetching wikitext:', error);
                                resolve();
                            });
                        } else resolve();
                    });
                    newNomPromise.then(async function () {
                        batches.forEach(batch => {
                            batchesToProcess.push({
                                titles: batch.titles,
                                textToModify: batch.prependText,
                                summary: taggingSummary,
                                type: 'prepend',
                                doneMessage: 'All pages edited.',
                                headingLabel: 'Editing nominated pages' + ((batches.length > 1) ? ' — ' + batch.label : '')
                            });
                        });
                        if (XFD === 'RFD') {
                            batchesToProcess.push({
                                id: 'rfd-notify-target',
                                titles: Object.fromEntries(Object.keys(redirectTargets).map(title => {
                                    let page = new mw.Title(redirectTargets[title]);
                                    return [page.getTalkPage().getPrefixedText(), title];
                                })), // return a map of page we want to edit : orginal title - this will (as a dicitionary) remove duplicates, but in an unpredictable way - it seems to retain the last one 
                                textToModify: redirectTargetNotification,
                                summary: redirectTargetNotificationSummary,
                                type: 'append',
                                doneMessage: 'All target talk pages notified.',
                                headingLabel: 'Notifying targets'
                            });
                        }
                        if (notifyCheckbox.isSelected()) {
                            batchesToProcess.push({
                                titles: authors,
                                textToModify: userNotification,
                                summary: userSummary,
                                type: 'append',
                                doneMessage: 'All users notified.',
                                headingLabel: 'Notifying users'
                            });
                        }
                        let promise = Promise.resolve();
                        // abort handling is now only in the editPage() function
                        for (const batch of batchesToProcess) {
                            // alert(`starting batch ${batch.headingLabel}`)
                            await processContent(batch);
                            // alert(`batch ${batch.headingLabel} done`)
                        }

                        promise.then(() => {
                            abortButton.setLabel('Revert');
                            // All done
                        }).catch(err => {
                            console.error('Error occurred:', err);
                        });
                    });

                });
            });
        }
    }

    // Run the script when the page is ready
    $(document).ready(runMassXFD);

}());
// </nowiki>