User:Polygnotus/Scripts/CheckImportedScripts.js

// <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>

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.