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>
// https://en.wikipedia.org/w/index.php?title=Giraffe&action=edit&typo=the&typofix=teh
//perhaps add some unit tests?

(function() {
    const DEBUG = false;
    function debug(...args) {
        if (DEBUG) {
            console.log('[TypoTool]', ...args);
        }
    }

    debug("Script started");

    const typo = getUrlParameter('typo');
    const typofix = getUrlParameter('typofix');
    if (!typo || !typofix || typo === '' || typofix === '') {
        debug("Typo or typofix parameters missing or empty. Script will not run.");
        return;
    }

    let hasRun = false;

    const disqualificationRegex = /((\[\s*sic\s*\]))|(\(\s*sic\s*\))|\{\s*sic\s*\}|\bsic\b|(&#91;sic&#93;)|(&#93;sic&#93;)|((\{\{\s*sic))(.*?)(\}\})|\{\{\s*bots\s*\}\}|\{\{\s*nobots\s*\}\}/i;

    const ignoreRegexes = [
        /((http|https):\/\/)(www.)?[-a-z0-9@:%._\+~#?&//=]{2,256}\.[-a-z]{2,26}\b([-a-z0-9@:%._\+~#?&//=]*)/i,
        /<blockquote>(.*?)<\/blockquote>/i,
        /((\{\{\s*DEFAULTSORT\s*:\s*))(.*?)((\}\}))/i,
        /<!--\s+(.*?)\s+-->/i,
        /(\[\[(File|Image):(.*?)(\.|\||\]\]))/i,
        /Image:(.*?)\./i,
        /Category:(.*?)\./i,
        /<\s*ref\s+name\s*=\s*"(.*?)"\s*>/i,
        /<\s*ref\s+name\s*=\s*(.*?)\s*>/i,
        /<\s*gallery\s*.*?<\/\s*gallery\s*>/i
    ];

    // URL regex (more comprehensive than the one in ignoreRegexes)
    const urlRegex = /((https?:\/\/)?(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gi;

    const ignoreTemplates = [
        "Interlanguage link multi", "Illm", "Bquote", "As written", "ILL", "Ill", "Proper name", "Transl",
        "Quote", "Quotation", "Block quote", "Notatypo", "III", "Chem name", "Bq", "Ill2", "Translit",
        "Nat", "Typo", "Not translated", "Not typo", "Lang-Latn", "Interlanguage", "\"", "Tlit",
        "Transliterate", "Cita", "Link-interwiki", "Citation bloc", "C quote", "Propername", "Quotes",
        "Quoteblock", "Cquote2", "Langue", "Interlanguage links", "Proper noun", "SIC", "Bug workaround",
        "ILLM", "LANG", "Blockquotation", "Block quotation", "LAng", "Zitat", "Cquotetxt", "Ill-wd",
        "Ill-WD", "Cquotetext", "PrettyQuotation", "Interlanguage link forced", "Lang-xx",
        "Long quotation", "Epigraph", "Red Wikidata link", "InterLanguage Link", "Gquote", "CquoteTxt",
        "Blockquote/old", "ISOtranslit", "RedQ", "NAT", "Belg", "Coquote", "Imagequote",
        "Block quote next to floating content", "Iw2", "MultiLink", "Gbq", "Interlanguage link Wikidata",
        "Imagequote2", "Interlanguage link", "lang", "transliteration", "blockquote", "not a typo", "sic", 
        "clarify", "Spoken Wikipedia", "Multiple image", "Double image", "Triple image", "Doubleimage", 
        "Tripleimage", "Multiple images", "Four images", "Auto images", "Autoimages", "Dual image", "Mehrere Bilder", 
        "Multipleimage", "Multiple iamge", "MImage", "Mimage", "Multimage", "Multiimage", "Mulitple images", "Multi image", 
        "Multi Image", "Multimg", "Double image stack", "Vertical images list", "Double images", "Multipleimages", "Multiple video", "Mim", "Cite tweet", "cite book"
    ];

    const ignoreParameters = [
        "reason", "trans-title", "first", "last", "name", "photo", "image", "title",
        "map_image", "image_skyline", "cover", "image_name", "last1", "first1","last2",
        "first2","last3", "first3","last4", "first4","last5", "first5","last6", "first6",
        "last7", "first7","last8", "first8","last9", "first9","last10", "first10", "logo", "structure1", "structure2", "author", "Ship image"
    ];

    function getUrlParameter(name) {
        name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
        var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
        var results = regex.exec(location.search);
        return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
    }

    function replaceTextInWikitextEditor(typo, typofix) {
        var maxAttempts = 20;
        var attemptInterval = 500; // 0.5 seconds

        function attempt(attemptsLeft) {
            var editTextarea = document.getElementById('wpTextbox1');
            if (editTextarea && editTextarea.value) {
                var currentText = editTextarea.value;

                // Check for automatic disqualification
                if (disqualificationRegex.test(currentText)) {
                    debug("Automatic disqualification found. Closing window.");
                    window.close();
                    return;
                }

                var typoPattern = new RegExp(typo, 'gi');
                var replacementsMade = false;
                
                var matches = [];
                var match;
                while ((match = typoPattern.exec(currentText)) !== null) {
                    matches.push({index: match.index, length: match[0].length, original: match[0]});
                }

                // Sort matches by length (descending) and then by index (ascending)
                matches.sort((a, b) => b.length - a.length || a.index - b.index);

                // Process matches in order, skipping overlaps
                var newText = currentText;
                var offset = 0;
                for (let match of matches) {
                    let adjustedIndex = match.index + offset;
                    if (shouldReplace(newText, adjustedIndex, match.original)) {
                        let replacement = match.original.charAt(0) === match.original.charAt(0).toUpperCase() 
                            ? typofix.charAt(0).toUpperCase() + typofix.slice(1)
                            : typofix.toLowerCase();
                        newText = newText.slice(0, adjustedIndex) + replacement + newText.slice(adjustedIndex + match.length);
                        offset += replacement.length - match.length;
                        replacementsMade = true;
                    }
                }

                editTextarea.value = newText;

                if (replacementsMade) {
                    var changeSummary = typo + ' → ' + typofix;
                    var editSummaryField = document.getElementById('wpSummary');
                    if (editSummaryField) {
                        editSummaryField.value = changeSummary;
                    }

                    var showChangesButton = document.querySelector('input[name="wpDiff"]');
                    if (showChangesButton) {
                        showChangesButton.click();
                        setTimeout(function() {
                            var form = showChangesButton.form;
                            if (form) form.submit();
                        }, 1000); // Increased timeout to 1 second
                    } else {
                        debug("Show changes button not found. Unable to submit changes.");
                    }
                    debug("Typo replaced and changes submitted.");
                } else {
                    debug("No replacements made. Closing window.");
                    window.close();
                }
            } else if (attemptsLeft > 0) {
                setTimeout(function() {
                    attempt(attemptsLeft - 1);
                }, attemptInterval);
            } else {
                debug("Edit textarea not found or empty after multiple attempts. Closing window.");
                window.close();
            }
        }

        attempt(maxAttempts);
    }

    function shouldReplace(text, index, match) {
        // Check if the match is within an HTML comment
        let commentStart = text.lastIndexOf('<!--', index);
        let commentEnd = text.indexOf('-->', index);
        if (commentStart !== -1 && commentEnd !== -1 && commentStart < index && index < commentEnd) {
            return false;
        }

        // Check if the match is within a URL
        let urlMatch = text.match(urlRegex);
        if (urlMatch) {
            for (let url of urlMatch) {
                let urlIndex = text.indexOf(url);
                if (urlIndex <= index && index < urlIndex + url.length) {
                    return false;
                }
            }
        }

        // Check if the match is within any of the ignore regexes
        for (let regex of ignoreRegexes) {
            let regexMatch = text.match(regex);
            if (regexMatch && regexMatch.index <= index && index < regexMatch.index + regexMatch[0].length) {
                return false;
            }
        }

        // Check if the match is within a template
        let templateDepth = 0;
        let i = index;
        while (i >= 0) {
            if (text.substr(i, 2) === '}}') {
                templateDepth++;
                i--;
            } else if (text.substr(i, 2) === '{{') {
                if (templateDepth === 0) {
                    let templateContent = text.substring(i + 2, index);
                    if (ignoreTemplates.some(template => templateContent.toLowerCase().startsWith(template.toLowerCase()))) {
                        return false;
                    }
                    break;
                }
                templateDepth--;
                i--;
            }
            i--;
        }

        // Check if the match is within a parameter
        let parameterDepth = 0;
        i = index;
        while (i >= 0) {
            if (text[i] === '|' && parameterDepth === 0) {
                let parameterContent = text.substring(i + 1, index);
                let equalSignPos = parameterContent.indexOf('=');
                if (equalSignPos !== -1) {
                    let parameterName = parameterContent.substring(0, equalSignPos).trim();
                    if (ignoreParameters.some(param => param.toLowerCase() === parameterName.toLowerCase())) {
                        return false;
                    }
                }
                break;
            } else if (text.substr(i, 2) === '}}') {
                parameterDepth++;
                i--;
            } else if (text.substr(i, 2) === '{{') {
                parameterDepth--;
                i--;
            }
            i--;
        }

        // Skip if it's just the correct word but with the first letter missing or the last one
        let typofix = getUrlParameter('typofix');
        if (match.toLowerCase() === typofix.toLowerCase().slice(1) || 
            match.toLowerCase() === typofix.toLowerCase().slice(0, -1)) {
            return false;
        }

        // Skip anything between [[File: and | or ]]
        let fileStart = text.lastIndexOf('[[File:', index);
        let fileSeparator = text.indexOf('|', index);
        let fileEnd = text.indexOf(']]', index);
        if (fileStart !== -1 && (fileSeparator !== -1 || fileEnd !== -1) && 
            fileStart < index && index < (fileSeparator !== -1 ? fileSeparator : fileEnd)) {
            return false;
        }

        // Skip anything between | logo = and | or ]] or a newline
        let logoStart = text.lastIndexOf('| logo =', index);
        let logoEnd = text.indexOf('|', index);
        if (logoEnd === -1) logoEnd = text.indexOf(']]', index);
        if (logoEnd === -1) logoEnd = text.indexOf('\n', index);
        if (logoStart !== -1 && logoEnd !== -1 && logoStart < index && index < logoEnd) {
            return false;
        }

        return true;
    }

    function init() {
        if (hasRun) return; // Prevent multiple executions
        hasRun = true;

        replaceTextInWikitextEditor(typo, typofix);
    }

    // Use both DOMContentLoaded and load events
    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", init);
    } else {
        init();
    }
    window.addEventListener("load", init);
})();
// </nowiki>