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.
// forked from https:https://www.how.com.vn/wiki/index.php?lang=en&q=User:%CE%A3/Testing_facility/Archiver.js&oldid=1003561411$.when( mw.loader.using(['mediawiki.util','mediawiki.api']), $.ready).done( function () {    if (mw.config.get("wgNamespaceNumber") % 2 == 0 && mw.config.get("wgNamespaceNumber") != 4) {        // not a talk page and not project namespace        return;    }    if (mw.config.get("wgNamespaceNumber") == -1) {        // is a special page        return;    }    mw.util.addCSS(".arky-selected-section { background-color:#D9E9FF } .arky-selected-section .arky-span a { font-weight:bold }");    var sectionCodepointOffsets = new Object();    var wikiText = "";    var revStamp; // The timestamp when we originally got the page contents - we pass it to the "edit" API call for edit conflict detection    var portletLink = mw.util.addPortletLink("p-cactions", "#", "ØCA", "pt-oeca", "Enter/exit the archival process", null, null);    var archiveButton = $(document.createElement("button"));    $(portletLink).click(function(e) {        $(".arky-selected-section").removeClass('.arky-selected-section');        $(".arky-span").toggle();        $("#arky-archive-button").toggle();    });    archiveButton.html("archive all the selected threads")        .attr("id", 'arky-archive-button')        .css("position", 'sticky')        .css("bottom", 0)        .css("width", '100%')        .css("font-size", '200%');    $(document.body).append(archiveButton);    archiveButton.toggle();    archiveButton.click(function(e) {        // returns `s` without the substring starting at `start` and ending at `end`        function cut(s, start, end) {            return s.substr(0, start) + s.substring(end);        }        var selectedSections = $(".arky-selected-section .arky-span").map(function() {            return $(this).data("section");        }).toArray();        if (selectedSections.length === 0) {            return alert("No threads selected, aborting");        }        var archivePageName = prompt("Archiving " + selectedSections.length + " threads: where should we move them to? (e.g. Wikipedia:Sandbox/Archive 1)", mw.config.get("wgPageName"));        if (!archivePageName || archivePageName == mw.config.get("wgPageName")) {            return alert("No archive target selected, aborting");        }        // codepointToUtf16Idx maps codepoint idx (i.e. MediaWiki index into page text) to utf-16 idx (i.e. JavaScript index into wikiText)        var codepointToUtf16Idx = {};        // Initialize "important" (= either a section start or end) values to 0        selectedSections.forEach(function(n) {            codepointToUtf16Idx[sectionCodepointOffsets[n].start] = 0;            codepointToUtf16Idx[sectionCodepointOffsets[n].end] = 0;        });        codepointToUtf16Idx[Infinity] = Infinity; // Because sometimes we'll have Infinity as an "end" value        // fill in our mapping from codepoints (MediaWiki indices) to utf-16 (i.e. JavaScript).        // yes, this loops through every character in the wikitext. very unfortunate.        var codepointPos = 0;        for (var utf16Pos = 0; utf16Pos < wikiText.length; utf16Pos++, codepointPos++) {            if (codepointToUtf16Idx.hasOwnProperty(codepointPos)) {                codepointToUtf16Idx[codepointPos] = utf16Pos;            }            if ((0xD800 <= wikiText.charCodeAt(utf16Pos)) && (wikiText.charCodeAt(utf16Pos) <= 0xDBFF)) {                // high surrogate! utf16Pos goes up by 2, but codepointPos goes up by only 1.                utf16Pos++; // skip the low surrogate            }        }        var newTextForArchivePage = selectedSections.map(function(n) {            return wikiText.substring(                codepointToUtf16Idx[sectionCodepointOffsets[n].start],                codepointToUtf16Idx[sectionCodepointOffsets[n].end]            );        }).join("");        selectedSections.reverse(); // go in reverse order so that we don't invalidate the offsets of earlier sections        var newWikiText = wikiText;        selectedSections.forEach(function(n) {            newWikiText = cut(                newWikiText,                codepointToUtf16Idx[sectionCodepointOffsets[n].start],                codepointToUtf16Idx[sectionCodepointOffsets[n].end]            );        });        console.log("archive this:" + newTextForArchivePage);        console.log("revised page:" + newWikiText);        var pluralizedThreads = selectedSections.length + ' thread' + ((selectedSections.length === 1) ? '' : 's');        new mw.Api().postWithToken("csrf", {            action: 'edit',            title: mw.config.get("wgPageName"),            text: newWikiText,            summary: "Removing " + pluralizedThreads + ", will be on [[" + archivePageName + "]]",            basetimestamp: revStamp,            starttimestamp: revStamp        }).done(function(res1) {            alert("Successfully removed threads from talk page");            console.log(res1);            new mw.Api().postWithToken("csrf", {action: 'edit', title: archivePageName, appendtext: "\n" + newTextForArchivePage, summary: "Adding " + pluralizedThreads + " from [[" + mw.config.get("wgPageName") + "]]"})                .done(function(res2) {                    alert("Successfully added threads to archive page");                })                .fail(function(res2) {                    alert("failed to add threads to archive page. manual inspection needed.");                })                .always(function(res2) {                    console.log(res2);                    window.location.reload();                });            })            .fail(function(res1) {                alert("failed to remove threads from talk page. aborting archive process.");                console.log(res1);                window.location.reload();            });    }); // end of archiveButton click handler    // grab page sections and wikitext so we can add the "archive" links to appropriate sections    new mw.Api().get({action: 'parse', page: mw.config.get("wgPageName")}).done(function(parseApiResult) {        new mw.Api().get({action: 'query', pageids: mw.config.get("wgArticleId"), prop: ['revisions'], rvprop: ['content', 'timestamp']}).done(function(revisionsApiResult) {            var rv;            rv = revisionsApiResult.query.pages[mw.config.get("wgArticleId")].revisions[0];            wikiText = rv["*"];            revStamp = rv['timestamp'];        });        var validSections = {};        $(parseApiResult.parse.sections)            // For sections transcluded from other pages, s.index will look            // like T-1 instead of just 1. Remove those.            .filter(function(i, s) { return s.index == parseInt(s.index) })            .each(function(i, s) { validSections[s.index] = s });        for (var i in validSections) {            i = parseInt(i);            // What MediaWiki calls "byteoffset" is actually a codepoint offset!! Drat!!            sectionCodepointOffsets[i] = {                start: validSections[i].byteoffset,                end: validSections.hasOwnProperty(i+1)?validSections[i+1].byteoffset:Infinity            };        }        $("#mw-content-text").find(":header").find("span.mw-headline").each(function(i, title) {            var header, headerLevel, editSection, sectionNumber;            header = $(this).parent();            headerLevel = header.prop("tagName").substr(1, 1) * 1; // wtf javascript            editSection = header.find(".mw-editsection"); // 1st child            var editSectionLink = header.find(".mw-editsection a:last");            var sectionNumber = undefined;            if (editSectionLink[0]) {                // Note: href may not be set.                var sectionNumberMatch = editSectionLink.attr("href") && editSectionLink.attr("href").match(/&section=(\d+)/);                if (sectionNumberMatch) {                    sectionNumber = sectionNumberMatch[1];                }            }            // if the if statement fails, it might be something like <h2>not a real section</h2>            if (validSections.hasOwnProperty(sectionNumber)){                $(editSection[0]).append(                    "&nbsp;",                    $("<span>", { "class": "arky-span" })                    .css({'display':'none'})                    .data({'header-level': headerLevel, 'section': sectionNumber})                    .append(                        $('<span>', { 'class': 'mw-editsection-bracket' }).text('['),                        $('<a>')                        .text('archive')                        .click(function(){                            var parentHeader = $(this).parents(':header');                            parentHeader.toggleClass('arky-selected-section');                            // now, click all sub-sections of this section                            var isThisSectionSelected = parentHeader.hasClass('arky-selected-section');                            var thisHeaderLevel = $(this).parents('.arky-span').data('header-level');                            // starting from the current section, loop through each section                            var allArchiveSpans = $('.arky-span');                            var currSectionIdx = allArchiveSpans.index($(this).parents('.arky-span'));                            for(var i = currSectionIdx + 1; i < allArchiveSpans.length; i++) {                                if($(allArchiveSpans[i]).data('header-level') <= thisHeaderLevel) {                                    // if this isn't a subsection, quit                                    break;                                }                                var closestHeader = $(allArchiveSpans[i]).parents(':header');                                if(closestHeader.hasClass('arky-selected-section') != isThisSectionSelected) {                                    // if this section needs toggling, toggle it                                    closestHeader.toggleClass('arky-selected-section');                                }                            }                            // finally, update button                            $('#arky-archive-button')                                .prop('disabled', !$('.arky-selected-section').length)                                .text('archive ' + $('.arky-selected-section').length + ' selected thread' +                                    (($('.arky-selected-section').length === 1) ? '' : 's'));                        }),                        $('<span>', { 'class': 'mw-editsection-bracket' }).text(']')                    ));            }        });    });});