User:Polygnotus/Scripts/CheckImportedScripts.js

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>
/*
On the wiki you run it it first checks common.js and then the skin-specific .js files, and then global.js on meta.
*/

(function() {
    'use strict';
    
    // Add link to Tools menu
    mw.loader.using(['mediawiki.util'], function() {
        mw.util.addPortletLink(
            'p-tb',
            '#',
            'Check imported scripts',
            't-check-scripts',
            'Verify that all imported scripts exist'
        );
        
        $('#t-check-scripts').click(function(e) {
            e.preventDefault();
            showUsernamePrompt();
        });
    });
    
    function showUsernamePrompt() {
        var currentUsername = mw.config.get('wgUserName');
        
        // Create prompt dialog
        var $promptOverlay = $('<div>')
            .css({
                position: 'fixed',
                top: 0,
                left: 0,
                right: 0,
                bottom: 0,
                backgroundColor: 'rgba(0,0,0,0.5)',
                zIndex: 9999
            });
        
        var $promptDialog = $('<div>')
            .css({
                position: 'fixed',
                top: '50%',
                left: '50%',
                transform: 'translate(-50%, -50%)',
                backgroundColor: 'white',
                border: '2px solid #a2a9b1',
                borderRadius: '4px',
                padding: '20px',
                minWidth: '400px',
                zIndex: 10000,
                boxShadow: '0 2px 10px rgba(0,0,0,0.3)'
            });
        
        var $promptTitle = $('<h3>').text('Check imported scripts').css('margin-top', 0);
        var $promptText = $('<p>').text('Whose scripts do you want to check?');
        
        var $buttonContainer = $('<div>').css({
            display: 'flex',
            gap: '10px',
            marginTop: '15px'
        });
        
        var $mineBtn = $('<button>')
            .text('My scripts')
            .css({
                padding: '8px 20px',
                flex: '1'
            })
            .click(function() {
                $promptDialog.remove();
                $promptOverlay.remove();
                checkScripts(currentUsername);
            });
        
        var $otherBtn = $('<button>')
            .text('Someone else\'s scripts')
            .css({
                padding: '8px 20px',
                flex: '1'
            })
            .click(function() {
                $promptDialog.remove();
                $promptOverlay.remove();
                showUsernameInput();
            });
        
        var $cancelBtn = $('<button>')
            .text('Cancel')
            .css({
                padding: '8px 20px',
                marginTop: '10px',
                width: '100%'
            })
            .click(function() {
                $promptDialog.remove();
                $promptOverlay.remove();
            });
        
        // ESC key to close
        $(document).on('keydown.promptDialog', function(e) {
            if (e.key === 'Escape') {
                $(document).off('keydown.promptDialog');
                $promptDialog.remove();
                $promptOverlay.remove();
            }
        });
        
        $buttonContainer.append($mineBtn, $otherBtn);
        $promptDialog.append($promptTitle, $promptText, $buttonContainer, $cancelBtn);
        $('body').append($promptOverlay, $promptDialog);
    }
    
    function showUsernameInput() {
        // Create input dialog
        var $inputOverlay = $('<div>')
            .css({
                position: 'fixed',
                top: 0,
                left: 0,
                right: 0,
                bottom: 0,
                backgroundColor: 'rgba(0,0,0,0.5)',
                zIndex: 9999
            });
        
        var $inputDialog = $('<div>')
            .css({
                position: 'fixed',
                top: '50%',
                left: '50%',
                transform: 'translate(-50%, -50%)',
                backgroundColor: 'white',
                border: '2px solid #a2a9b1',
                borderRadius: '4px',
                padding: '20px',
                minWidth: '400px',
                zIndex: 10000,
                boxShadow: '0 2px 10px rgba(0,0,0,0.3)'
            });
        
        var $inputTitle = $('<h3>').text('Enter account name').css('margin-top', 0);
        var $inputLabel = $('<label>').text('Account name:').css({
            display: 'block',
            marginBottom: '5px'
        });
        
        var $usernameInput = $('<input>')
            .attr('type', 'text')
            .css({
                width: '100%',
                padding: '8px',
                border: '1px solid #a2a9b1',
                borderRadius: '3px',
                boxSizing: 'border-box'
            });
        
        var $errorMsg = $('<div>').css({
            color: 'red',
            marginTop: '10px',
            display: 'none'
        });
        
        var $buttonContainer = $('<div>').css({
            display: 'flex',
            gap: '10px',
            marginTop: '15px'
        });
        
        var $checkBtn = $('<button>')
            .text('Check')
            .css({
                padding: '8px 20px',
                flex: '1'
            })
            .click(function() {
                var username = $usernameInput.val().trim();
                if (!username) {
                    $errorMsg.text('Please enter a username').show();
                    return;
                }
                $(document).off('keydown.inputDialog');
                $inputDialog.remove();
                $inputOverlay.remove();
                checkScripts(username);
            });
        
        var $cancelBtn = $('<button>')
            .text('Cancel')
            .css({
                padding: '8px 20px',
                flex: '1'
            })
            .click(function() {
                $(document).off('keydown.inputDialog');
                $inputDialog.remove();
                $inputOverlay.remove();
            });
        
        // Allow Enter key to submit
        $usernameInput.keypress(function(e) {
            if (e.which === 13) {
                $checkBtn.click();
            }
        });
        
        // ESC key to close
        $(document).on('keydown.inputDialog', function(e) {
            if (e.key === 'Escape') {
                $(document).off('keydown.inputDialog');
                $inputDialog.remove();
                $inputOverlay.remove();
            }
        });
        
        $buttonContainer.append($checkBtn, $cancelBtn);
        $inputDialog.append($inputTitle, $inputLabel, $usernameInput, $errorMsg, $buttonContainer);
        $('body').append($inputOverlay, $inputDialog);
        
        $usernameInput.focus();
    }
    
    function checkScripts(username) {
        // Get the current wiki for comparison
        var currentWiki = window.location.hostname;
        
        // Files to check
        var jsFiles = [
            'common.js',
            'vector.js',
            'vector-2022.js',
            'minerva.js',
            'monobook.js',
            'timeless.js',
            'global.js'
        ];
        
        // Interwiki prefix mapping - comprehensive list from Wikipedia
        var interwikiMap = {
            // Shorthand prefixes
            'c': 'commons.wikimedia.org',
            'm': 'meta.wikimedia.org',
            'meta': 'meta.wikimedia.org',
            'd': 'www.wikidata.org',
            'f': 'www.wikifunctions.org',
            'w': 'en.wikipedia.org',
            'wikt': 'en.wiktionary.org',
            'q': 'en.wikiquote.org',
            'b': 'en.wikibooks.org',
            'n': 'en.wikinews.org',
            's': 'en.wikisource.org',
            'v': 'en.wikiversity.org',
            'voy': 'en.wikivoyage.org',
            
            // Full names
            'commons': 'commons.wikimedia.org',
            'wikidata': 'www.wikidata.org',
            'wikifunctions': 'www.wikifunctions.org',
            'wikipedia': 'en.wikipedia.org',
            'wiktionary': 'en.wiktionary.org',
            'wikiquote': 'en.wikiquote.org',
            'wikibooks': 'en.wikibooks.org',
            'wikinews': 'en.wikinews.org',
            'wikisource': 'en.wikisource.org',
            'wikiversity': 'en.wikiversity.org',
            'wikivoyage': 'en.wikivoyage.org',
            'species': 'species.wikimedia.org',
            'wikispecies': 'species.wikimedia.org',
            
            // MediaWiki and related
            'mw': 'www.mediawiki.org',
            'mediawikiwiki': 'www.mediawiki.org',
            'wikitech': 'wikitech.wikimedia.org',
            'labsconsole': 'wikitech.wikimedia.org',
            
            // Meta and Foundation
            'metawiki': 'meta.wikimedia.org',
            'metawikimedia': 'meta.wikimedia.org',
            'metawikipedia': 'meta.wikimedia.org',
            'foundation': 'foundation.wikimedia.org',
            'wikimedia': 'foundation.wikimedia.org',
            'wmf': 'foundation.wikimedia.org',
            
            // Incubator and testing
            'incubator': 'incubator.wikimedia.org',
            'betawikiversity': 'beta.wikiversity.org',
            'testwiki': 'test.wikipedia.org',
            'test2wiki': 'test2.wikipedia.org',
            'testwikidata': 'test.wikidata.org',
            
            // Outreach and other
            'outreach': 'outreach.wikimedia.org',
            'outreachwiki': 'outreach.wikimedia.org',
            'wikimania': 'wikimania.wikimedia.org',
            'diff': 'diff.wikimedia.org',
            'diffblog': 'diff.wikimedia.org',
            'donate': 'donate.wikimedia.org',
            
            // Phabricator and development
            'phab': 'phabricator.wikimedia.org',
            'phabricator': 'phabricator.wikimedia.org',
            'gerrit': 'gerrit.wikimedia.org',
            
            // Chapter wikis (some examples)
            'translatewiki': 'translatewiki.net',
            'betawiki': 'translatewiki.net'
        };
        
        // Normalize page title (replace underscores with spaces, but preserve case)
        function normalizeTitle(title) {
            return title.replace(/_/g, ' ');
        }
        
        // Parse interwiki prefix
        function parseInterwiki(path) {
            var match = path.match(/^([a-z0-9-]+):(.+)$/i);
            if (match) {
                var prefix = match[1].toLowerCase();
                var remainder = match[2];
                
                if (interwikiMap[prefix]) {
                    var targetWiki = interwikiMap[prefix];
                    var currentWiki = window.location.hostname;
                    
                    // If the interwiki prefix points to the current wiki, treat it as a local namespace instead
                    if (targetWiki === currentWiki) {
                        return {
                            wiki: null,
                            page: normalizeTitle(path)
                        };
                    }
                    
                    return {
                        wiki: targetWiki,
                        page: normalizeTitle(remainder)
                    };
                }
            }
            
            return {
                wiki: null,
                page: normalizeTitle(path)
            };
        }
        
        // Get URL for a page
        function getPageUrl(title, wiki) {
            if (wiki) {
                // For external wikis, we need to properly encode the title
                // Encode each component separately to preserve the structure
                var parts = title.split('/');
                var encodedParts = parts.map(function(part) {
                    return encodeURIComponent(part).replace(/%20/g, '_').replace(/%3A/g, ':');
                });
                return 'https://' + wiki + '/wiki/' + encodedParts.join('/');
            } else {
                return mw.util.getUrl(title);
            }
        }
        
        // Extract importScript and mw.loader.load calls from text
        // Improved to handle comments, multi-line statements, and arrays
        function extractScripts(text) {
            var scripts = [];
            
            // Remove single-line comments (but not // inside strings)
            // Match // only when not inside quotes
            var cleanedText = text.replace(/(['"])(?:(?=(\\?))\2.)*?\1|\/\/.*/g, function(match) {
                // If it's a quoted string, keep it; otherwise it's a comment, remove it
                return match.startsWith("'") || match.startsWith('"') ? match : '';
            });
            // Remove multi-line comments
            cleanedText = cleanedText.replace(/\/\*[\s\S]*?\*\//g, '');
            
            // Match importScript('...') or importScript("...")
            // Catches ALL importScript calls regardless of content
            var importScriptRegex = /importScript\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
            var match;
            while ((match = importScriptRegex.exec(cleanedText)) !== null) {
                var parsed = parseInterwiki(match[1]);
                scripts.push({
                    type: 'script',
                    path: match[1],
                    normalizedPath: parsed.page,
                    wiki: parsed.wiki
                });
            }
            
            // Match mw.loader.load with scripts (single string)
            // Catch both .js files and URLs with title parameter
            var loaderRegex = /mw\.loader\.load\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
            while ((match = loaderRegex.exec(cleanedText)) !== null) {
                var originalPath = match[1];
                var item = originalPath;
                var wiki = null;
                
                // Extract domain from protocol-relative or full URLs
                var domainMatch = item.match(/^(?:https?:)?\/\/([^/]+)/);
                if (domainMatch) {
                    wiki = domainMatch[1];
                }
                
                // Check if it's a MediaWiki index.php URL with title parameter
                var titleMatch = item.match(/[?&]title=([^&]+)/);
                if (titleMatch) {
                    item = decodeURIComponent(titleMatch[1]);
                } else {
                    // Check if it's a /wiki/ URL
                    var wikiMatch = item.match(/\/wiki\/([^?#]+)/);
                    if (wikiMatch) {
                        item = decodeURIComponent(wikiMatch[1]);
                    } else if (!item.match(/\.js/)) {
                        // Skip items that don't look like scripts
                        continue;
                    }
                }
                
                // If the extracted wiki matches the current wiki, treat as local
                var currentWiki = window.location.hostname;
                if (wiki === currentWiki) {
                    wiki = null;
                }
                
                // Parse the extracted page title (not the original URL)
                var parsed = parseInterwiki(item);
                scripts.push({
                    type: 'script',
                    path: originalPath, // Keep original path for display
                    normalizedPath: parsed.page,
                    wiki: wiki || parsed.wiki
                });
            }
            
            // Match mw.loader.load with arrays
            var loaderArrayRegex = /mw\.loader\.load\s*\(\s*\[([^\]]+)\]/g;
            while ((match = loaderArrayRegex.exec(cleanedText)) !== null) {
                var arrayContent = match[1];
                var itemRegex = /['"]([^'"]+)['"]/g;
                var itemMatch;
                while ((itemMatch = itemRegex.exec(arrayContent)) !== null) {
                    var originalPath = itemMatch[1];
                    var item = originalPath;
                    var wiki = null;
                    
                    // Extract domain from protocol-relative or full URLs
                    var domainMatch = item.match(/^(?:https?:)?\/\/([^/]+)/);
                    if (domainMatch) {
                        wiki = domainMatch[1];
                    }
                    
                    // Check if it's a MediaWiki index.php URL with title parameter
                    var titleMatch = item.match(/[?&]title=([^&]+)/);
                    if (titleMatch) {
                        item = decodeURIComponent(titleMatch[1]);
                    } else {
                        // Check if it's a /wiki/ URL
                        var wikiMatch = item.match(/\/wiki\/([^?#]+)/);
                        if (wikiMatch) {
                            item = decodeURIComponent(wikiMatch[1]);
                        } else if (!item.match(/\.js/)) {
                            // Skip items that don't look like scripts
                            continue;
                        }
                    }
                    
                    // If the extracted wiki matches the current wiki, treat as local
                    var currentWiki = window.location.hostname;
                    if (wiki === currentWiki) {
                        wiki = null;
                    }
                    
                    // Parse the extracted page title (not the original URL)
                    var parsed = parseInterwiki(item);
                    scripts.push({
                        type: 'script',
                        path: originalPath, // Keep original path for display
                        normalizedPath: parsed.page,
                        wiki: wiki || parsed.wiki
                    });
                }
            }
            
            return scripts;
        }
        
        // Create a unique key for a script (normalized path + wiki)
        function getScriptKey(script) {
            return (script.wiki || 'local') + '::' + script.normalizedPath;
        }
        
        // Normalize script key for cross-file duplicate detection
        // This handles the case where global.js references scripts on the current wiki
        function getNormalizedScriptKey(script, sourceFile) {
            var wiki = script.wiki;
            
            // If this script is from global.js and references the current wiki,
            // treat it as a local script for comparison purposes
            if (sourceFile === 'global.js' && wiki === currentWiki) {
                wiki = null;
            }
            
            return (wiki || 'local') + '::' + script.normalizedPath;
        }
        
        // Find duplicates within a single file
        function findInternalDuplicates(scripts) {
            var seen = {};
            var duplicates = [];
            
            scripts.forEach(function(script) {
                var key = getScriptKey(script);
                if (seen[key]) {
                    duplicates.push(script);
                } else {
                    seen[key] = true;
                }
            });
            
            return duplicates;
        }
        
        // Check if a page exists (just check existence, ignore case and redirects)
        function checkPageExists(title, wiki) {
            var apiUrl = wiki 
                ? 'https://' + wiki + '/w/api.php'
                : mw.util.wikiScript('api');
            
            return $.ajax({
                url: apiUrl,
                data: {
                    action: 'query',
                    titles: title,
                    redirects: true,
                    format: 'json',
                    formatversion: 2,
                    origin: '*'
                },
                dataType: 'json',
                timeout: 10000,
                headers: {
                    'Api-User-Agent': 'CheckImportedScripts/1.0'
                }
            }).then(function(data) {
                var page = data.query.pages[0];
                
                // Just check if page exists - MediaWiki handles case and redirects automatically
                return !page.missing;
            }).catch(function(jqXHR, textStatus, errorThrown) {
                // Provide specific error messages
                var errorMsg;
                if (textStatus === 'timeout') {
                    errorMsg = 'Request timed out';
                } else if (textStatus === 'error') {
                    if (jqXHR.status === 0) {
                        errorMsg = 'Network error (no connection or CORS issue)';
                    } else if (jqXHR.status === 404) {
                        errorMsg = 'API endpoint not found (404)';
                    } else if (jqXHR.status === 403) {
                        errorMsg = 'Access forbidden (403)';
                    } else if (jqXHR.status >= 500) {
                        errorMsg = 'Server error (' + jqXHR.status + ')';
                    } else {
                        errorMsg = 'HTTP error ' + jqXHR.status;
                    }
                } else if (textStatus === 'parsererror') {
                    errorMsg = 'Failed to parse API response';
                } else {
                    errorMsg = textStatus || 'Unknown error';
                }
                throw new Error(errorMsg);
            });
        }
        
        // Create popup
        var $popup = $('<div>')
            .css({
                position: 'fixed',
                top: '50%',
                left: '50%',
                transform: 'translate(-50%, -50%)',
                backgroundColor: 'white',
                border: '2px solid #a2a9b1',
                borderRadius: '4px',
                padding: '20px',
                minWidth: '500px',
                maxWidth: '700px',
                maxHeight: '80vh',
                overflow: 'auto',
                zIndex: 10000,
                boxShadow: '0 2px 10px rgba(0,0,0,0.3)'
            });
        
        var $overlay = $('<div>')
            .css({
                position: 'fixed',
                top: 0,
                left: 0,
                right: 0,
                bottom: 0,
                backgroundColor: 'rgba(0,0,0,0.5)',
                zIndex: 9999
            });
        
        var $title = $('<h3>').text('Checking scripts for User:' + username).css('margin-top', 0);
        var $progress = $('<div>').css({
            fontFamily: 'monospace',
            fontSize: '12px',
            backgroundColor: '#f8f9fa',
            padding: '10px',
            borderRadius: '3px',
            maxHeight: '400px',
            overflow: 'auto',
            marginTop: '10px'
        });
        var $summary = $('<div>').css({
            marginTop: '15px',
            padding: '10px',
            backgroundColor: '#f0f0f0',
            borderRadius: '3px',
            fontWeight: 'bold'
        });
        var $closeBtn = $('<button>')
            .text('Close')
            .css({
                marginTop: '15px',
                padding: '5px 15px'
            })
            .click(function() {
                $(document).off('keydown.resultDialog');
                $popup.remove();
                $overlay.remove();
            });
        
        $popup.append($title, $progress, $summary, $closeBtn);
        $('body').append($overlay, $popup);
        
        // ESC key to close
        $(document).on('keydown.resultDialog', function(e) {
            if (e.key === 'Escape') {
                $(document).off('keydown.resultDialog');
                $popup.remove();
                $overlay.remove();
            }
        });
        
        function addProgress(message, color) {
            var $line = $('<div>').html(message).css('color', color || '#000');
            $progress.append($line);
            $progress.scrollTop($progress[0].scrollHeight);
        }
        
        // Main checking function
        var allMissing = [];
        var allInternalDuplicates = [];
        var allScriptsByFile = {}; // Store scripts for cross-file duplicate checking
        
        // Process JS files sequentially to show progress
        function processJsFile(index) {
            if (index >= jsFiles.length) {
                // All files processed, now check for cross-file duplicates
                checkCrossFileDuplicates();
                return;
            }
            
            var jsFile = jsFiles[index];
            var pageName = 'User:' + username + '/' + jsFile;
            
            // If checking global.js, use Meta-Wiki
            var targetWiki = (jsFile === 'global.js') ? 'meta.wikimedia.org' : null;
            var fileUrl = getPageUrl(pageName, targetWiki);
            
            var displayName = jsFile;
            if (targetWiki) {
                displayName += ' <small style="color: #666;">(on ' + targetWiki + ')</small>';
            }
            
            addProgress('Checking <a href="' + fileUrl + '" target="_blank" style="color: #0645ad;"><strong>' + displayName + '</strong></a>...', '#000');
            
            // Use the appropriate API
            var apiCall;
            if (targetWiki) {
                apiCall = $.ajax({
                    url: 'https://' + targetWiki + '/w/api.php',
                    data: {
                        action: 'query',
                        titles: pageName,
                        prop: 'revisions',
                        rvprop: 'content',
                        format: 'json',
                        formatversion: 2,
                        origin: '*'
                    },
                    dataType: 'json',
                    headers: {
                        'Api-User-Agent': 'CheckImportedScripts/1.0'
                    },
                    xhrFields: {
                        withCredentials: false
                    }
                });
            } else {
                apiCall = new mw.Api().get({
                    action: 'query',
                    titles: pageName,
                    prop: 'revisions',
                    rvprop: 'content',
                    formatversion: 2
                });
            }
            
            apiCall.then(function(data) {
                var page = data.query.pages[0];
                
                if (page.missing) {
                    addProgress('  → Page does not exist', '#888');
                    allScriptsByFile[jsFile] = [];
                    // 1 second delay before next file
                    setTimeout(function() {
                        processJsFile(index + 1);
                    }, 1000);
                    return;
                }
                
                if (!page.revisions) {
                    addProgress('  → No content found', '#888');
                    allScriptsByFile[jsFile] = [];
                    // 1 second delay before next file
                    setTimeout(function() {
                        processJsFile(index + 1);
                    }, 1000);
                    return;
                }
                
                addProgress('  → Page exists', '#080');
                
                var content = page.revisions[0].content;
                var scripts = extractScripts(content);
                
                // Store scripts for cross-file checking
                allScriptsByFile[jsFile] = scripts.map(function(s) {
                    return {
                        script: s,
                        sourceFile: jsFile,
                        sourceWiki: targetWiki
                    };
                });
                
                if (scripts.length === 0) {
                    addProgress('  → No scripts found', '#888');
                    // 1 second delay before next file
                    setTimeout(function() {
                        processJsFile(index + 1);
                    }, 1000);
                    return;
                }
                
                addProgress('  → Found ' + scripts.length + ' script(s)', '#000');
                
                // Check for internal duplicates
                var internalDupes = findInternalDuplicates(scripts);
                if (internalDupes.length > 0) {
                    addProgress('  → ⚠ Found ' + internalDupes.length + ' duplicate(s) within this file', '#f80');
                    internalDupes.forEach(function(dupe) {
                        var scriptUrl = getPageUrl(dupe.normalizedPath, dupe.wiki);
                        allInternalDuplicates.push({
                            script: dupe,
                            sourceFile: jsFile,
                            sourceWiki: targetWiki
                        });
                        addProgress('  &nbsp;&nbsp;⚠ <a href="' + scriptUrl + '" target="_blank" style="color: #f80;">' + dupe.path + '</a>', '#f80');
                    });
                }
                
                addProgress('  → Verifying script existence...', '#000');
                
                // Check scripts sequentially
                var scriptIndex = 0;
                var missingInFile = 0;
                
                function checkNextScript() {
                    if (scriptIndex >= scripts.length) {
                        if (missingInFile === 0) {
                            addProgress('  → All scripts exist', '#080');
                        } else {
                            addProgress('  → ' + missingInFile + ' missing', '#c00');
                        }
                        addProgress(''); // blank line
                        
                        // Process next file after 1 second delay
                        setTimeout(function() {
                            processJsFile(index + 1);
                        }, 1000);
                        return;
                    }
                    
                    var script = scripts[scriptIndex];
                    script.sourceFile = jsFile;
                    script.sourceWiki = targetWiki;
                    
                    // Show progress: "Checking script X of Y"
                    addProgress('  &nbsp;&nbsp;Checking script ' + (scriptIndex + 1) + ' of ' + scripts.length + '...', '#888');
                    
                    checkPageExists(script.normalizedPath, script.wiki).then(function(exists) {
                        var scriptUrl = getPageUrl(script.normalizedPath, script.wiki);
                        
                        if (!exists) {
                            addProgress('  &nbsp;&nbsp;✗ <a href="' + scriptUrl + '" target="_blank" style="color: #c00;"><strong>' + script.path + '</strong></a> (MISSING)', '#c00');
                            allMissing.push(script);
                            missingInFile++;
                        } else {
                            addProgress('  &nbsp;&nbsp;✓ <a href="' + scriptUrl + '" target="_blank" style="color: #080;">' + script.path + '</a>', '#080');
                        }
                        
                        scriptIndex++;
                        // 1 second delay between script checks
                        setTimeout(checkNextScript, 1000);
                    }).catch(function(error) {
                        addProgress('  &nbsp;&nbsp;⚠ ' + script.path + ' (Error: ' + error.message + ')', '#f80');
                        scriptIndex++;
                        // 1 second delay between script checks
                        setTimeout(checkNextScript, 1000);
                    });
                }
                
                checkNextScript();
                
            }).catch(function(error) {
                var errorMsg = error.message || error.statusText || error.toString();
                addProgress('  → Error: ' + errorMsg, '#c00');
                allScriptsByFile[jsFile] = [];
                // 1 second delay before next file
                setTimeout(function() {
                    processJsFile(index + 1);
                }, 1000);
            });
        }
        
        // Check for cross-file duplicates
        function checkCrossFileDuplicates() {
            addProgress('Checking for cross-file duplicates...', '#000');
            
            var skinFiles = ['vector.js', 'vector-2022.js', 'minerva.js', 'monobook.js', 'timeless.js'];
            var commonFiles = ['common.js', 'global.js'];
            
            var crossFileDuplicates = [];
            
            // Build script index for common.js and global.js using normalized keys
            var commonScripts = {};
            commonFiles.forEach(function(commonFile) {
                if (allScriptsByFile[commonFile]) {
                    allScriptsByFile[commonFile].forEach(function(item) {
                        var key = getNormalizedScriptKey(item.script, commonFile);
                        if (!commonScripts[key]) {
                            commonScripts[key] = [];
                        }
                        commonScripts[key].push({
                            file: commonFile,
                            wiki: item.sourceWiki,
                            originalScript: item.script
                        });
                    });
                }
            });
            
            // First, check for duplicates between common.js and global.js themselves
            Object.keys(commonScripts).forEach(function(key) {
                var files = commonScripts[key];
                if (files.length > 1) {
                    // Found duplicate between common files
                    for (var i = 0; i < files.length; i++) {
                        for (var j = i + 1; j < files.length; j++) {
                            // Only report if it's actually different files
                            if (files[i].file !== files[j].file) {
                                crossFileDuplicates.push({
                                    script: files[i].originalScript,
                                    file1: files[i].file,
                                    wiki1: files[i].wiki,
                                    file2: files[j].file,
                                    wiki2: files[j].wiki,
                                    commonScript: files[j].originalScript
                                });
                            }
                        }
                    }
                }
            });
            
            // Check each skin file for duplicates with common.js or global.js
            skinFiles.forEach(function(skinFile) {
                if (allScriptsByFile[skinFile]) {
                    allScriptsByFile[skinFile].forEach(function(item) {
                        var key = getNormalizedScriptKey(item.script, skinFile);
                        if (commonScripts[key]) {
                            // Found duplicate
                            commonScripts[key].forEach(function(commonFileInfo) {
                                crossFileDuplicates.push({
                                    script: item.script,
                                    file1: skinFile,
                                    wiki1: item.sourceWiki,
                                    file2: commonFileInfo.file,
                                    wiki2: commonFileInfo.wiki,
                                    commonScript: commonFileInfo.originalScript
                                });
                            });
                        }
                    });
                }
            });
            
            if (crossFileDuplicates.length > 0) {
                addProgress('  → ⚠ Found ' + crossFileDuplicates.length + ' cross-file duplicate(s)', '#f80');
                crossFileDuplicates.forEach(function(dupe) {
                    var scriptUrl = getPageUrl(dupe.script.normalizedPath, dupe.script.wiki);
                    var file1Display = dupe.file1 + (dupe.wiki1 ? ' (on ' + dupe.wiki1 + ')' : '');
                    var file2Display = dupe.file2 + (dupe.wiki2 ? ' (on ' + dupe.wiki2 + ')' : '');
                    
                    // Show both the original paths for clarity
                    var path1 = dupe.script.path;
                    var path2 = dupe.commonScript.path;
                    
                    if (path1 === path2) {
                        addProgress('  &nbsp;&nbsp;⚠ <a href="' + scriptUrl + '" target="_blank" style="color: #f80;">' + path1 + '</a> appears in both ' + file1Display + ' and ' + file2Display, '#f80');
                    } else {
                        addProgress('  &nbsp;&nbsp;⚠ <a href="' + scriptUrl + '" target="_blank" style="color: #f80;">' + path1 + '</a> (in ' + file1Display + ') is the same as <strong>' + path2 + '</strong> (in ' + file2Display + ')', '#f80');
                    }
                });
            } else {
                addProgress('  → No cross-file duplicates found', '#080');
            }
            
            addProgress(''); // blank line
            
            // Show final summary
            showFinalSummary(crossFileDuplicates);
        }
        
        // Show final summary
        function showFinalSummary(crossFileDuplicates) {
            var summaryHtml = '';
            var hasIssues = false;
            
            if (allMissing.length > 0) {
                hasIssues = true;
                summaryHtml += '<div style="margin-bottom: 15px;"><strong style="color: #c00;">✗ Found ' + allMissing.length + ' missing script(s):</strong><br>';
                summaryHtml += '<ul style="margin: 5px 0; padding-left: 20px; text-align: left;">';
                allMissing.forEach(function(script) {
                    var scriptUrl = getPageUrl(script.normalizedPath, script.wiki);
                    summaryHtml += '<li style="margin: 5px 0;"><a href="' + scriptUrl + '" target="_blank"><strong>' + script.path + '</strong></a>';
                    if (script.wiki) {
                        summaryHtml += ' <small style="color: #666;">(on ' + script.wiki + ')</small>';
                    }
                    var fileUrl = getPageUrl('User:' + username + '/' + script.sourceFile, script.sourceWiki);
                    summaryHtml += '<br><small style="color: #666;">Found in: <a href="' + fileUrl + '" target="_blank">' + script.sourceFile + '</a>';
                    if (script.sourceWiki) {
                        summaryHtml += ' <small style="color: #666;">(on ' + script.sourceWiki + ')</small>';
                    }
                    summaryHtml += '</small></li>';
                });
                summaryHtml += '</ul></div>';
            }
            
            if (allInternalDuplicates.length > 0) {
                hasIssues = true;
                summaryHtml += '<div style="margin-bottom: 15px;"><strong style="color: #f80;">⚠ Found ' + allInternalDuplicates.length + ' internal duplicate(s):</strong><br>';
                summaryHtml += '<ul style="margin: 5px 0; padding-left: 20px; text-align: left;">';
                allInternalDuplicates.forEach(function(item) {
                    var scriptUrl = getPageUrl(item.script.normalizedPath, item.script.wiki);
                    var fileUrl = getPageUrl('User:' + username + '/' + item.sourceFile, item.sourceWiki);
                    summaryHtml += '<li style="margin: 5px 0;"><a href="' + scriptUrl + '" target="_blank">' + item.script.path + '</a>';
                    if (item.script.wiki) {
                        summaryHtml += ' <small style="color: #666;">(on ' + item.script.wiki + ')</small>';
                    }
                    summaryHtml += '<br><small style="color: #666;">Duplicate in: <a href="' + fileUrl + '" target="_blank">' + item.sourceFile + '</a>';
                    if (item.sourceWiki) {
                        summaryHtml += ' <small style="color: #666;">(on ' + item.sourceWiki + ')</small>';
                    }
                    summaryHtml += '</small></li>';
                });
                summaryHtml += '</ul></div>';
            }
            
            if (crossFileDuplicates.length > 0) {
                hasIssues = true;
                summaryHtml += '<div style="margin-bottom: 15px;"><strong style="color: #f80;">⚠ Found ' + crossFileDuplicates.length + ' cross-file duplicate(s):</strong><br>';
                summaryHtml += '<ul style="margin: 5px 0; padding-left: 20px; text-align: left;">';
                crossFileDuplicates.forEach(function(dupe) {
                    var scriptUrl = getPageUrl(dupe.script.normalizedPath, dupe.script.wiki);
                    var file1Url = getPageUrl('User:' + username + '/' + dupe.file1, dupe.wiki1);
                    var file2Url = getPageUrl('User:' + username + '/' + dupe.file2, dupe.wiki2);
                    summaryHtml += '<li style="margin: 5px 0;"><a href="' + scriptUrl + '" target="_blank">' + dupe.script.path + '</a>';
                    if (dupe.script.wiki) {
                        summaryHtml += ' <small style="color: #666;">(on ' + dupe.script.wiki + ')</small>';
                    }
                    summaryHtml += '<br><small style="color: #666;">Found in: <a href="' + file1Url + '" target="_blank">' + dupe.file1 + '</a>';
                    if (dupe.wiki1) {
                        summaryHtml += ' (on ' + dupe.wiki1 + ')';
                    }
                    summaryHtml += ' and <a href="' + file2Url + '" target="_blank">' + dupe.file2 + '</a>';
                    if (dupe.wiki2) {
                        summaryHtml += ' (on ' + dupe.wiki2 + ')';
                    }
                    summaryHtml += '</small></li>';
                });
                summaryHtml += '</ul></div>';
            }
            
            if (!hasIssues) {
                summaryHtml = '✓ All scripts exist and no duplicates found!';
                $summary.html(summaryHtml).css('color', 'green');
            } else {
                $summary.html(summaryHtml).css('color', '#000');
            }
            
            $title.text('Check complete - User:' + username);
        }
        
        // Start processing
        processJsFile(0);
    }
})();
// </nowiki>