Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
// ---- Bharatpedia-specific config -----------------------------------------
const BP_DOC_PAGE = 'MediaWiki:Gadget-OneClickArchiver';
const BP_SCRIPT_TAG = 'OneClickArchiver';
// --------------------------------------------------------------------------

const ui_strings = {};
ui_strings['en'] = {
    archive_link_text: 'Archive',
    archive_to_text: 'Archive to:',

    oca_status_enabled: 'OCA is enabled.',
    oca_status_disabled_category: 'OCA is disabled: page is in category <a href="/wiki/Category:$1" target="_blank">$1</a>.',
    oca_status_disabled_nottalk: 'OCA is disabled: this does not appear to be a talkpage.',
    oca_status_using_template: 'OCA is using configuration from template',
    oca_status_no_config: 'OCA is enabled on this page; however, OCA could not find any existing archiving configuration.',

    oca_loading_init: 'OCA is loading.',
    oca_loading_looking_for_config: 'OCA is looking for configuration.',
    oca_loading_looking_for_archive: 'OCA is looking for the next valid archive page.',
    oca_loading_adding_archive_links: 'OCA is adding archive links.',

    oca_status_heading: 'OneClickArchiver status',

    oca_info_button_name: 'OCA info',
    oca_info_button_description: 'Information about how OneClickArchiver is behaving on this page',

    archivethis_retrieving_content: 'Retrieving section content.',
    archivethis_content_retrieved: 'Section content retrieved.',
    archivethis_dnau_message: 'This section has been marked "Do Not Archive Until" $1, so archiving was aborted.',
    archivethis_dnau_see_docs: `Please see <a href="/wiki/${BP_DOC_PAGE}" title="${BP_DOC_PAGE}" target="_blank">the documentation</a> for details.`,
    archivethis_dnau_aborted: 'OneClickArchiver aborted!',
    archivethis_writing_existing_page: 'Writing to existing archive page <a href="/wiki/$1" target="_blank">$1</a>.',
    archivethis_writing_new_page: 'Writing to new archive page <a class="new" href="/wiki/$1" target="_blank">$1</a>.',
    archivethis_es_archived_using: `archived using [[${BP_DOC_PAGE}|OneClickArchiver]]`,
    archivethis_es_to: 'to',
    archivethis_successfully_added: 'Successfully added to archive. Removing from source page.',
    archivethis_success: 'Successfully removed from source page. Updating archive config if needed...',
};

const ui_lang = window.OCAUILang !== undefined ? window.OCAUILang : 'en';
const ui_str = ui_strings[ui_lang];
if (!ui_str) {
    alert("You are trying to use OCA with a language that does not exist! Script will not work.");
}

const no_manual_archiving_categories = ['Pages that should not be manually archived'];
const automatically_signed_nontalk_categories = ['Non-talk pages that are automatically signed'];

var linkSize;
if (!window.OCALinkSize) {
    linkSize = 0.6;
} else {
    linkSize = window.OCALinkSize;
}

// ---- utility functions ---------------------------------------------------
function findTemplateInPageRaw(pageText, templateName) {
    const escapedName = templateName.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
    const templateRegex = new RegExp(
        `(\\{\\{\\s*${escapedName}\\s*\\|)((?:[^{}]|\\{\\{(?:[^{}]|\\{\\{[^{}]*\\}\\})*\\}\\})*)(\\}\\})`,
        'i'
    );
    const match = pageText.match(templateRegex);
    if (!match) return null;
    return match;
}

function findTemplateInPage(pageText, templateName) {
    const match = findTemplateInPageRaw(pageText, templateName);
    if (!match) return null;
    return match[2];
}

function parseTemplateParamsRaw(inputTemplateString) {
    const params = [];
    let currentParam = '';
    let braceDepth = 0;
    for (let i = 0; i < inputTemplateString.length; i++) {
        const char = inputTemplateString[i];
        if (char === '{' && inputTemplateString[i + 1] === '{') {
            braceDepth++;
            currentParam += '\x7B\x7B';
            i++;
        } else if (char === '}' && inputTemplateString[i + 1] === '}') {
            braceDepth--;
            currentParam += '\x7D\x7D';
            i++;
        } else if (char === '|' && braceDepth === 0) {
            params.push(currentParam);
            currentParam = '';
        } else {
            currentParam += char;
        }
    }
    if (currentParam) params.push(currentParam);
    return params;
}

function parseTemplateParams(inputTemplateString, dontStripWhitespace) {
    const params = parseTemplateParamsRaw(inputTemplateString);
    const result = {};
    params.forEach(param => {
        const eqIndex = param.indexOf('=');
        if (eqIndex !== -1) {
            const key = param.substring(0, eqIndex).trim();
            const valueBase = param.substring(eqIndex + 1);
            const value = dontStripWhitespace
                ? valueBase.replace(/^[\r\n]+|[\r\n]+$/g, '')
                : valueBase.trim();
            if (key) result[key] = value;
        }
    });
    return result;
}

function updateTemplateParamInPage(pageText, templateName, targetKey, newValue) {
    const [fullMatch, prefix, content, suffix] = findTemplateInPageRaw(pageText, templateName);
    const params = parseTemplateParamsRaw(content);
    let keyFound = false;
    const updatedParams = params.map(param => {
        const eqIndex = param.indexOf('=');
        if (eqIndex !== -1) {
            const key = param.substring(0, eqIndex).trim();
            if (key === targetKey) {
                keyFound = true;
                const beforeEq = param.substring(0, eqIndex);
                const afterEq = param.substring(eqIndex + 1);
                const spaceMatch = afterEq.match(/^(\s*)/);
                const leadingSpaces = spaceMatch ? spaceMatch[1] : '';
                const trailingSpaceMatch = afterEq.match(/(\s*)$/);
                const trailingSpaces = trailingSpaceMatch ? trailingSpaceMatch[1] : '';
                return `${beforeEq}=${leadingSpaces}${newValue}${trailingSpaces}`;
            }
        }
        return param;
    });
    if (!keyFound) updatedParams.push(`${targetKey} = ${newValue}\n`);
    const updatedTemplate = prefix + updatedParams.join('|') + suffix;
    return pageText.replace(fullMatch, updatedTemplate);
}

mw.loader.using(['mediawiki.util', 'mediawiki.api'], async function () {

    var OCATagIfAvailable;
    var config = mw.config.get([
        'debug', 'wgAction', 'wgArticleId', 'wgCategories', 'wgMonthNames',
        'wgNamespaceNumber', 'wgPageName', 'wgRelevantUserName'
    ]);

    var OCAStatus, OCALoading;

    function determinePageArchivability() {
        const categories = config.wgCategories || [];
        OCAStatus = ui_str.oca_status_enabled;
        for (const categoryName of no_manual_archiving_categories) {
            if (categories.includes(categoryName)) {
                OCAStatus = ui_str.oca_status_disabled_category.replaceAll('$1', categoryName);
                return false;
            }
        }
        for (const categoryName of automatically_signed_nontalk_categories) {
            if (categories.includes(categoryName)) return true;
        }
        if (document.querySelector('#ca-addsection')) return true;
        const ns = config.wgNamespaceNumber;
        if (ns === 1 || ns === 3 || ns === 5 || ns === 7 || ns === 9 || ns === 11 || ns === 13 || ns === 15) {
            return true;
        }
        OCAStatus = ui_str.oca_status_disabled_nottalk;
        return false;
    }

    async function doesTagExist(tagName) {
        try {
            const response = await new mw.Api().get({
                action: 'query',
                list: 'tags',
                tgprop: 'active|source',
                tgcontinue: tagName,
                tglimit: 1
            });
            const tags = response.query && response.query.tags;
            if (tags && tags.length > 0 && tags[0].name === tagName) {
                const targetTag = tags[0];
                return targetTag.active !== undefined && targetTag.source.includes('manual');
            }
            return false;
        } catch (error) {
            return false;
        }
    }

    async function archiveThis(sectionNumber, archiveName, currentArchiveName, sectionName, archiveConfig) {
        document.body.insertAdjacentHTML(
            'beforeend',
            '<div class="overlay" style="background-color: #000000; opacity: 0.4; position: fixed; top: 0px; left: 0px; width: 100%; height: 100%; z-index: 500;"></div>'
        );
        document.body.insertAdjacentHTML(
            'afterbegin',
            '<div class="arcProg" style="font-weight: bold; box-shadow: 7px 7px 5px #000000; font-size: 0.9em; line-height: 1.5em; z-index: 501; opacity: 1; position: fixed; width: 50%; left: 25%; top: 30%; background: #F7F7F7; border: #222222 ridge 1px; padding: 20px;"></div>'
        );
        const arcProg = document.querySelector('.arcProg');
        function printMessage(message, color) {
            if (!color) color = "black";
            arcProg.insertAdjacentHTML('beforeend', `<div style="color: ${color}">${message}</div>`);
        }

        const pageid = config.wgArticleId;
        const api = new mw.Api();
        printMessage(ui_str.archivethis_retrieving_content);

        const sectionResponse = await api.get({
            action: 'query',
            pageids: pageid,
            rvsection: sectionNumber,
            prop: ['revisions', 'info'],
            rvprop: 'content',
            indexpageids: 1,
            rawcontinue: ''
        });

        var sectionContent = sectionResponse.query.pages[pageid].revisions[0]['*'];
        printMessage(ui_str.archivethis_content_retrieved, "green");

        var dnau = sectionContent.match(/<!-- \[\[User:DoNotArchiveUntil\]\] ([\d]{2}):([\d]{2}),? ([\d]{1,2}) (January|February|March|April|May|June|July|August|September|October|November|December) ([\d]{4}) \(UTC\) -->/);
        var dnauDate;
        if (dnau === null || dnau === undefined) {
            dnauDate = Date.now();
            dnau = null;
        } else {
            dnau = dnau[1] + ':' + dnau[2] + ' ' + dnau[3] + ' ' + dnau[4] + ' ' + dnau[5];
            dnauDate = new Date(dnau);
            dnauDate = dnauDate.valueOf();
        }

        if (dnauDate > Date.now()) {
            document.querySelectorAll('.arcProg, .overlay').forEach(element => element.remove());
            const dnauAbortMsg = document.createElement('p');
            dnauAbortMsg.innerHTML = `${ui_str.archivethis_dnau_message.replaceAll('$1', dnau)}<br /><br /><span style="font-size: larger;">${ui_str.archivethis_dnau_see_docs}</span>`;
            mw.notify(dnauAbortMsg, { title: ui_str.archivethis_dnau_aborted, tag: 'OCAdnau', autoHide: false });
            return;
        }

        if (archiveName == currentArchiveName) {
            sectionContent = '\n\n\x7B\x7BClear\x7D\x7D\n' + sectionContent;
            printMessage(ui_str.archivethis_writing_existing_page.replaceAll('$1', archiveName));
        } else {
            sectionContent = archiveConfig.archivePageHeader + '\n\n' + sectionContent;
            printMessage(ui_str.archivethis_writing_new_page.replaceAll('$1', archiveName));
        }

        if (dnau != null) {
            sectionContent = sectionContent.replace(/<!-- \[\[User:DoNotArchiveUntil\]\] ([\d]{2}):([\d]{2}), ([\d]{1,2}) (January|February|March|April|May|June|July|August|September|October|November|December) ([\d]{4}) \(UTC\) -->/g, '');
        }

        await api.postWithToken('edit', {
            action: 'edit',
            title: archiveName,
            appendtext: sectionContent,
            summary: `/* ${sectionName} */ ${ui_str.archivethis_es_archived_using}`,
            tags: OCATagIfAvailable
        });

        printMessage(ui_str.archivethis_successfully_added, "green");
        await new mw.Api().postWithToken('edit', {
            action: 'edit',
            section: sectionNumber,
            pageid: pageid,
            text: '',
            summary: `[[${BP_DOC_PAGE}|OneClickArchived]] "${sectionName}" ${ui_str.archivethis_es_to} [[${archiveName}]]`,
            tags: OCATagIfAvailable
        });
        printMessage(ui_str.archivethis_success, "green");
        await archiveConfig.updateConfigIfNeeded(pageid);
        location.reload();
    }

    function addArchiveLinks(headerLevel, archiveName, currentArchiveName, archiveConfig) {
        var headings = document.querySelectorAll('h' + headerLevel);
        Array.prototype.forEach.call(headings, function (heading) {
            if (heading.querySelector('.archiverLink')) return;
            var parentWrapper = heading.parentElement;
            if (parentWrapper && parentWrapper.querySelector && parentWrapper.querySelector('.archiverLink')) return;

            var editsection = heading.querySelector('.mw-editsection');
            if (!editsection && parentWrapper) {
                editsection = parentWrapper.querySelector('.mw-editsection');
            }
            if (!editsection) return;

            var editLink = editsection.querySelector('a:not(.mw-editsection-visualeditor)');
            if (!editLink) return;

            var editSectionURL = editLink.getAttribute('href');
            if (!editSectionURL) return;
            var urlParams = new URLSearchParams(editSectionURL);
            var section = urlParams.get('section');

            var sectionNumber = 0;
            if (section && section.indexOf('T') === -1) {
                sectionNumber = parseInt(section, 10);
            } else {
                return;
            }

            var sectionName = heading.textContent.replace(/\[?\s*edit\s*\]?\s*$/i, '').trim();

            var archiveButtonWrapper = document.createElement('span');
            archiveButtonWrapper.className = 'archiverLinkWrapper';
            archiveButtonWrapper.style.fontSize = linkSize + 'em';
            archiveButtonWrapper.style.fontWeight = 'bold';
            archiveButtonWrapper.style.marginLeft = '0.5em';

            var archiveButton = document.createElement('a');
            archiveButton.href = '#archiverLink';
            archiveButton.className = 'archiverLink';
            archiveButton.setAttribute('data-section', String(sectionNumber));
            archiveButton.title = ui_str.archive_to_text + ' ' + archiveName;
            archiveButton.innerText = ui_str.archive_link_text;

            archiveButton.addEventListener('click', function (e) {
                e.preventDefault();
                archiveThis(sectionNumber, archiveName, currentArchiveName, sectionName, archiveConfig);
            });

            archiveButtonWrapper.appendChild(document.createTextNode('[ '));
            archiveButtonWrapper.appendChild(archiveButton);
            archiveButtonWrapper.appendChild(document.createTextNode(' ]'));

            if (parentWrapper && parentWrapper.classList && parentWrapper.classList.contains('mw-heading')) {
                parentWrapper.appendChild(archiveButtonWrapper);
            } else {
                heading.appendChild(archiveButtonWrapper);
            }
        });
    }

    // ---- config classes --------------------------------------------------
    class archiveBotConfig {
        constructor(counter, archivePageHeader, headerLevel) {
            this.counter = parseInt(counter);
            if (!counter) this.counter = 1;
            this.archivePageHeader = archivePageHeader;
            this.headerLevel = headerLevel;
        }
        getCurrentArchiveName() { throw new Error("You must implement this method!"); }
        getArchiveNameToWrite() { throw new Error("You must implement this method!"); }
        async updateConfigIfNeeded() { throw new Error("You must implement this method!"); }
    }

    class archiveBotConfigMisza extends archiveBotConfig {
        _parseToBytes(sizeString) {
            if (typeof sizeString !== 'string') return null;
            const cleanStr = sizeString.trim().toUpperCase();
            const match = cleanStr.match(/^(\d+(?:\.\d+)?)\s*([KMGTPB]?)$/);
            if (!match) return null;
            const value = parseFloat(match[1]);
            const unit = match[2];
            const multipliers = { '': 1, 'B': 1, 'K': 1e3, 'M': 1e6, 'G': 1e9 };
            return value * (multipliers[unit] || 1);
        }

        _applyPythonTemplateSubstitutions(inputTemplate, counter) {
            const now = new Date();
            const thisMonthNum = now.getMonth() + 1;
            const thisYear = now.getFullYear();
            const thisMonthFullName = config.wgMonthNames[thisMonthNum];
            const monthNamesShort = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
            const thisMonthShortName = monthNamesShort[thisMonthNum];
            const thisQuarter = Math.ceil(thisMonthNum / 4);

            const values = {
                year: thisYear,
                month: thisMonthNum,
                monthname: thisMonthFullName,
                monthnameshort: thisMonthShortName,
                quarter: thisQuarter,
                counter: counter
            };

            const placeholderRegex = /%\((\w+)\)(0?\d+)?([ds])/g;
            return inputTemplate.replace(placeholderRegex, (match, key, pad) => {
                if (!(key in values)) return match;
                let val = String(values[key]);
                if (pad) {
                    const padChar = pad.startsWith('0') ? '0' : ' ';
                    const padLength = parseInt(pad, 10);
                    val = val.padStart(padLength, padChar);
                }
                return val;
            });
        }

        constructor(counter, archiveheader, archive, maxarchivesize, templateName, pageText) {
            const headerLevel = 2;
            if (!archiveheader) archiveheader = "\x7B\x7BArchive\x7D\x7D";
            super(counter, archiveheader, headerLevel);
            this.pageText = pageText;
            this.templateName = templateName;
            this.parsedCounter = counter;
            this.archivePythonTemplate = archive;
            this.archiveSizeLimitType = null;
            if (maxarchivesize && maxarchivesize.endsWith("T")) {
                this.archiveSizeThreadLimit = parseInt(maxarchivesize.slice(0, -1));
                if (this.archiveSizeThreadLimit) this.archiveSizeLimitType = "threads";
            } else {
                this.archiveSizeByteLimit = this._parseToBytes(maxarchivesize);
                if (this.archiveSizeByteLimit) this.archiveSizeLimitType = "bytes";
            }
        }

        getCurrentArchiveName() {
            return this._applyPythonTemplateSubstitutions(this.archivePythonTemplate, this.counter);
        }

        getArchiveNameToWrite(currentArchiveBytes, currentArchiveSections, counterExtra) {
            this.toWriteCounter = this.counter + counterExtra;
            if (this.archiveSizeLimitType === "threads") {
                if (currentArchiveSections >= this.archiveSizeThreadLimit) this.toWriteCounter++;
            } else if (this.archiveSizeLimitType === "bytes") {
                if (currentArchiveBytes >= this.archiveSizeByteLimit) this.toWriteCounter++;
            }
            return this._applyPythonTemplateSubstitutions(this.archivePythonTemplate, this.toWriteCounter);
        }

        async updateConfigIfNeeded(pageid) {
            if (!this.templateName) return;
            const newContent = updateTemplateParamInPage(this.pageText, this.templateName, "counter", this.toWriteCounter);
            if (newContent != this.pageText) {
                await new mw.Api().postWithToken('edit', {
                    action: 'edit',
                    section: 0,
                    pageid: pageid,
                    text: newContent,
                    summary: `[[${BP_DOC_PAGE}|OneClickArchiver]] updating counter.`,
                    tags: OCATagIfAvailable
                });
            }
        }
    }

    function parseNativeArchiveConfig(pageText) {
        const templateName = "Archive config";
        const content = findTemplateInPage(pageText, templateName);
        if (!content) return;
        const cfg = parseTemplateParams(content);
        if (!cfg.archive) return;
        return new archiveBotConfigMisza(
            cfg.counter,
            cfg.header,
            cfg.archive,
            cfg.maxsize,
            templateName,
            pageText
        );
    }

    function createDefaultConfig(pageName, pageText) {
        const archivePattern = pageName + '/Archive %(counter)d';
        return new archiveBotConfigMisza(
            1,
            '\x7B\x7BArchive\x7D\x7D',
            archivePattern,
            '100K',
            null,
            pageText
        );
    }

    function parseMiszaBotConfig(pageText) {
        const templateName = "User:MiszaBot/config";
        const content = findTemplateInPage(pageText, templateName);
        if (!content) return;
        const config = parseTemplateParams(content);
        return new archiveBotConfigMisza(config.counter, config.archiveheader, config.archive, config.maxarchivesize, templateName, pageText);
    }
    function parseArchiveBasics(pageText) {
        const templateName = "Archive basics";
        const content = findTemplateInPage(pageText, templateName);
        if (!content) return;
        const config = parseTemplateParams(content);
        return new archiveBotConfigMisza(config.counter, config.archiveheader, config.archive, config.maxarchivesize, templateName, pageText);
    }
    function parseHazardBotConfig(pageText) {
        const templateName = "User:Hazard-Bot/Archiver";
        const content = findTemplateInPage(pageText, templateName);
        if (!content) return;
        const config = parseTemplateParams(content);
        return new archiveBotConfigMisza(config.counter, config.archiveheader, config.archive, config.maxarchivesize, templateName, pageText);
    }

    class archiveBotConfigClueBotIII extends archiveBotConfig {
        _phpDate(format, timestamp) {
            const date = timestamp !== undefined
                ? (timestamp instanceof Date ? timestamp : new Date(timestamp * 1000))
                : new Date();
            const pad = (val, len = 2) => String(val).padStart(len, '0');
            const getDayOfYear = (d) => {
                const start = new Date(d.getFullYear(), 0, 0);
                const diff = d - start;
                const oneDay = 1000 * 60 * 60 * 24;
                return Math.floor(diff / oneDay) - 1;
            };
            const getISOWeek = (d) => {
                const target = new Date(d.valueOf());
                const dayNr = (d.getDay() + 6) % 7;
                target.setDate(target.getDate() - dayNr + 3);
                const firstThursday = target.valueOf();
                target.setMonth(0, 1);
                if (target.getDay() !== 4) target.setMonth(0, 1 + ((4 - target.getDay() + 7) % 7));
                return 1 + Math.ceil((firstThursday - target) / 604800000);
            };
            const getSwatchTime = (d) => {
                const time = d.getTime();
                const utc1 = time + (d.getTimezoneOffset() * 60000) + 3600000;
                const bmt = new Date(utc1);
                const beats = Math.floor((bmt.getHours() * 3600 + bmt.getMinutes() * 60 + bmt.getSeconds() + bmt.getMilliseconds() / 1000) / 86.4);
                return pad(beats, 3);
            };
            const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
            const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
            const formatters = {
                d: d => pad(d.getDate()),
                D: d => days[d.getDay()].substring(0, 3),
                j: d => d.getDate(),
                l: d => days[d.getDay()],
                N: d => d.getDay() === 0 ? 7 : d.getDay(),
                S: d => { const j = d.getDate(); if (j % 10 === 1 && j !== 11) return 'st'; if (j % 10 === 2 && j !== 12) return 'nd'; if (j % 10 === 3 && j !== 13) return 'rd'; return 'th'; },
                w: d => d.getDay(),
                z: d => getDayOfYear(d),
                W: d => pad(getISOWeek(d)),
                F: d => months[d.getMonth()],
                m: d => pad(d.getMonth() + 1),
                M: d => months[d.getMonth()].substring(0, 3),
                n: d => d.getMonth() + 1,
                t: d => new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate(),
                L: d => (new Date(d.getFullYear(), 1, 29).getMonth() === 1) ? 1 : 0,
                o: d => { const target = new Date(d.valueOf()); target.setDate(target.getDate() - ((d.getDay() + 6) % 7) + 3); return target.getFullYear(); },
                Y: d => d.getFullYear(),
                y: d => String(d.getFullYear()).slice(-2),
                a: d => d.getHours() >= 12 ? 'pm' : 'am',
                A: d => d.getHours() >= 12 ? 'PM' : 'AM',
                B: d => getSwatchTime(d),
                g: d => d.getHours() % 12 || 12,
                G: d => d.getHours(),
                h: d => pad(d.getHours() % 12 || 12),
                H: d => pad(d.getHours()),
                i: d => pad(d.getMinutes()),
                s: d => pad(d.getSeconds()),
                u: d => pad(d.getMilliseconds() * 1000, 6),
                v: d => pad(d.getMilliseconds(), 3),
                e: d => { try { return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; } catch (e) { return 'UTC'; } },
                I: d => { const jan = new Date(d.getFullYear(), 0, 1).getTimezoneOffset(); const jul = new Date(d.getFullYear(), 6, 1).getTimezoneOffset(); return d.getTimezoneOffset() < Math.max(jan, jul) ? 1 : 0; },
                O: d => { const offset = d.getTimezoneOffset(); const sign = offset > 0 ? '-' : '+'; const absOffset = Math.abs(offset); return sign + pad(Math.floor(absOffset / 60)) + pad(absOffset % 60); },
                P: d => { const offset = d.getTimezoneOffset(); const sign = offset > 0 ? '-' : '+'; const absOffset = Math.abs(offset); return sign + pad(Math.floor(absOffset / 60)) + ':' + pad(absOffset % 60); },
                p: d => { const offset = d.getTimezoneOffset(); if (offset === 0) return 'Z'; const sign = offset > 0 ? '-' : '+'; const absOffset = Math.abs(offset); return sign + pad(Math.floor(absOffset / 60)) + ':' + pad(absOffset % 60); },
                T: d => { const matches = d.toTimeString().match(/\(([^)]+)\)$/); return matches ? matches[1] : 'UTC'; },
                Z: d => -d.getTimezoneOffset() * 60,
                c: d => d.toISOString().replace(/\.\d+Z$/, '') + (d.getTimezoneOffset() === 0 ? 'Z' : formatters.P(d)),
                r: d => d.toUTCString(),
                U: d => Math.floor(d.getTime() / 1000)
            };
            let result = '';
            for (let i = 0; i < format.length; i++) {
                const char = format[i];
                if (char === '\\') {
                    if (i + 1 < format.length) result += format[++i];
                    continue;
                }
                if (formatters[char]) result += formatters[char](date);
                else result += char;
            }
            return result;
        }

        _renderPHPDateString(dateString, counter) {
            const dateStringCountersApplied = dateString.replace('%%i', counter);
            return this._phpDate(dateStringCountersApplied);
        }

        constructor(archiveprefix, format, header, numberstart, headerlevel, maxarchsize, templateName, pageText) {
            if (!headerlevel) headerlevel = 2;
            if (!header) header = "\x7B\x7BArchive\x7D\x7D";
            super(numberstart, header, headerlevel);
            this.archiveprefix = archiveprefix;
            this.format = format;
            this.maxarchsize = maxarchsize || Infinity;
            this.templateName = templateName;
            this.pageText = pageText;
        }

        getCurrentArchiveName() {
            const archiveSuffix = this._renderPHPDateString(this.format, this.counter);
            return this.archiveprefix + archiveSuffix;
        }

        getArchiveNameToWrite(currentArchiveBytes, currentArchiveSections, counterExtra) {
            var toWriteCounter = this.counter + counterExtra;
            if (this.format.includes('%%i')) {
                if (currentArchiveBytes >= this.maxarchsize) toWriteCounter++;
            }
            const archiveSuffix = this._renderPHPDateString(this.format, toWriteCounter);
            return this.archiveprefix + archiveSuffix;
        }

        async updateConfigIfNeeded() { return; }
    }

    function parseClueBotIIIConfig(pageText) {
        const templateName = "User:ClueBot III/ArchiveThis";
        const content = findTemplateInPage(pageText, templateName);
        if (!content) return;
        const config = parseTemplateParams(content, true);
        return new archiveBotConfigClueBotIII(config.archiveprefix, config.format, config.header, config.numberstart, config.headerlevel, config.maxarchsize, templateName, pageText);
    }

    const archiveConfigsToTry = [
        parseNativeArchiveConfig,
        parseMiszaBotConfig,
        parseClueBotIIIConfig,
        parseArchiveBasics,
        parseHazardBotConfig
    ];

    function OCAInfo() {
        const OCAInfoMsg = document.createElement('p');
        OCAInfoMsg.innerHTML = `<b>${OCALoading}</b>${OCAStatus}`;
        mw.notify(OCAInfoMsg, { title: ui_str.oca_status_heading, tag: 'OCAstatus', autoHide: false });
    }

    async function getPageSizeInfo(pageName, headerLevel) {
        const api = new mw.Api();
        const [pageData, pageParse] = await Promise.all([
            api.get({
                action: 'query',
                prop: 'revisions',
                rvlimit: 1,
                rvprop: ['size', 'content'],
                titles: pageName,
                list: 'usercontribs',
                uclimit: 1,
                ucprop: 'timestamp',
                ucuser: config.wgRelevantUserName || 'Example',
                rawcontinue: '',
            }),
            api.get({
                action: 'parse',
                page: pageName,
                prop: 'tocdata',
            }).catch(error => {
                if (error === 'missingtitle') return { parse: { tocdata: { sections: [] } } };
                throw error;
            })
        ]);

        const page = Object.values((pageData && pageData.query && pageData.query.pages) || {})[0];
        const pageBytes = parseInt(page && page.revisions && page.revisions[0] && page.revisions[0].size, 10) || -1;
        const sections = (pageParse && pageParse.parse && pageParse.parse.tocdata && pageParse.parse.tocdata.sections) || [];
        const TOClevel = headerLevel - 1;
        const pageSections = sections.filter(s => parseInt(s.tocLevel, 10) === TOClevel).length;
        return [pageBytes, pageSections];
    }

    async function loadOCA() {
        var OCATagExistsPromise = doesTagExist(BP_SCRIPT_TAG);
        OCALoading = `${ui_str.oca_loading_init}<br />`;

        if (mw.config.get('wgNamespaceNumber') > 0) {
            const OCAInfoButton = mw.util.addPortletLink(
                'p-cactions',
                '#archiverLink',
                ui_str.oca_info_button_name,
                'pt-OCA',
                ui_str.oca_info_button_description,
                null
            );
            OCAInfoButton.addEventListener('click', function (e) {
                e.preventDefault();
                OCAInfo();
            });
        }

        if (!determinePageArchivability()) {
            OCALoading = "";
            return;
        }

        var pageid = config.wgArticleId;
        const pageSection0 = await new mw.Api().get({
            action: 'query',
            prop: ['revisions', 'info'],
            rvsection: 0,
            rvprop: 'content',
            pageids: pageid,
            indexpageids: 1,
            rawcontinue: ''
        });

        const content0 = pageSection0.query.pages[pageid].revisions[0]['*'];

        OCALoading = `${ui_str.oca_loading_looking_for_config}<br />\n`;
        var archiveConfig;
        for (const configLoader of archiveConfigsToTry) {
            archiveConfig = configLoader(content0);
            if (archiveConfig) {
                const templateQualifiedName = archiveConfig.templateName.includes(":") ? archiveConfig.templateName : "Template:" + archiveConfig.templateName;
                OCAStatus = OCAStatus + `\n${ui_str.oca_status_using_template} \x7B\x7B<a href="/wiki/${templateQualifiedName}">${archiveConfig.templateName}</a>\x7D\x7D.`;
                break;
            }
        }

        if (!archiveConfig) {
            archiveConfig = createDefaultConfig(config.wgPageName, content0);
            OCAStatus = OCAStatus + ` <i>(no config template found; using default: ${config.wgPageName}/Archive N)</i>`;
        }

        const initialArchiveName = archiveConfig.getCurrentArchiveName();

        OCALoading = `${ui_str.oca_loading_looking_for_archive}<br />\n`;
        var archiveNameLast;
        var archiveNameCurrent = initialArchiveName;
        var counterExtra = 0;
        while (archiveNameLast !== archiveNameCurrent) {
            archiveNameLast = archiveNameCurrent;
            const [currentArchiveBytes, currentArchiveSections] = await getPageSizeInfo(archiveNameLast, archiveConfig.headerLevel);
            archiveNameCurrent = archiveConfig.getArchiveNameToWrite(currentArchiveBytes, currentArchiveSections, counterExtra);
            counterExtra++;
        }

        const archivePageToWrite = archiveNameCurrent;

        if (await OCATagExistsPromise) {
            OCATagIfAvailable = BP_SCRIPT_TAG;
        }

        OCALoading = `${ui_str.oca_loading_adding_archive_links}<br />\n`;
        addArchiveLinks(archiveConfig.headerLevel, archivePageToWrite, initialArchiveName, archiveConfig);
        OCALoading = "";
    }

    if (document.readyState !== 'loading') {
        loadOCA();
    } else {
        document.addEventListener('DOMContentLoaded', async () => {
            loadOCA();
        });
    }
});