MediaWiki:Common.js

Revision as of 17:43, 30 June 2025 by MarkWD (talk | contribs) (// via Wikitext Extension for VSCode)

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

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
// Load jQuery UI using mw.loader
mw.loader.using(['jquery.ui'], function() {
    console.log("jQuery UI loaded");
    
    // Initialize and set up responsive handling when DOM is ready
    $(function() {
        setupResponsiveMenus();
});

// ANCHOR: Open all external links on a new window
mw.loader.using(['mediawiki.util']).then(function () {
    function openExternalLinks() {
        // Define internal actions that should not open in a new tab
        const internalLinkActions = [
            'edit', 
            'history', 
            'delete', 
            'submit', // For forms like Special:UserLogin, Special:Upload
            'watch', 
            'unwatch', 
            'protect', 
            'unprotect', 
            'markpatrolled',
            'purge', // action=purge
            'render' // action=render
        ];
        const currentHostname = window.location.hostname;
        // General external links
        document.querySelectorAll('a.external').forEach(function (link) {
            try {
                // Ensure link.href is an absolute URL before parsing
                // If link.href is relative, new URL() needs a base
                const linkUrl = new URL(link.href); 
                const linkAction = linkUrl.searchParams.get('action');
                // Check if it's an internal link AND has one of the specified actions
                if (linkUrl.hostname === currentHostname && linkAction && internalLinkActions.includes(linkAction)) {
                    // It's an internal action link, don't open in a new tab
                    return; 
                }
                // For all other links with class 'external'
                link.setAttribute('target', '_blank');
                link.setAttribute('rel', 'noopener noreferrer');
            } catch (e) {
                // Fallback for non-HTTP/HTTPS links or if URL parsing fails
                console.warn('JS/Common.js: Could not parse URL for link, applying default external link behavior for "a.external":', link.href, e);
                link.setAttribute('target', '_blank');
                link.setAttribute('rel', 'noopener noreferrer');
            }
        });

        // Social media links within templates
        document.querySelectorAll('div.external-social a, div.ntldstats a').forEach(function (link) {
            link.setAttribute('target', '_blank');
            link.setAttribute('rel', 'noopener noreferrer');
        });
    }
    mw.hook('wikipage.content').add(openExternalLinks);
});

// ANCHOR: Attach classes to system links for CSS customization
mw.hook('wikipage.content').add(function($content) {
    $content.find('a[title="Special:Undelete"]').addClass('undelete-link');
  });  

// ANCHOR: Include a registration link in the Minerva/mobile hamburger menu for logged out users
mw.loader.using(['mediawiki.util'], function() {
    $(function() {
      if (mw.config.get('skin') === 'minerva' && mw.user.isAnon()) {
        var waitForPersonalMenu = function(callback) {
          var $menu = $('#p-personal');
          if ($menu.length) {
            callback($menu);
          } else {
            setTimeout(function() {
              waitForPersonalMenu(callback);
            }, 100);
          }
        };
        waitForPersonalMenu(function($menu) {
          var regUrl = mw.util.getUrl('Special:UserLogin', { type: 'signup' });
          var regText = "Request account";
          var $regItem = $('<li>')
            .addClass('toggle-list-item menu__item--register')
            .append(
              $('<a>')
                .addClass('toggle-list-item__anchor')
                .attr('href', regUrl)
                .append(
                  $('<span class="minerva-icon minerva-icon--logIn"></span>').css('color','var(--general-link-color)')
                )
                .append(
                  $('<span class="toggle-list-item__label"></span>').text(regText).css('color','var(--general-link-color)')
                )
            );
          $menu.append($regItem);
        });
      }
    });
  });

// ANCHOR: Pre-fill registration page biography box with minimum requirements information
$(function() {
    var $bio = $("#wpBio");
    if ($bio.length && !$bio.val()) {
        $bio.attr("placeholder", "Minimum 10 words");
    }
});

// ANCHOR: Category alphabetical sorter (Adapted from Fandom)
(function(){
  if (window.mediaWikiCategorySorterLoaded) return;
  window.mediaWikiCategorySorterLoaded = true;

  function sorter(a, b) {
    return (a.textContent || '').localeCompare(b.textContent || '');
  }

  $(function() {
    $('#catlinks .mw-normal-catlinks, #catlinks .mw-hidden-catlinks').each(function() {
      var $list  = $(this).find('ul'),
          $items = $list.children('li'),
          sorted = $items.sort(sorter);

      $list.empty().append(sorted);
    });
  });
})();

// ANCHOR: Modified "Gadget-predefined-summaries"
// Lists interactive common edit reasons below "Summary"
mw.loader.using(['mediawiki.util']).then(function () {
    'use strict';

    if (window.resumeDeluxe === undefined &&
        ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
        mw.util.getParamValue('section') !== 'new') {

        var resumeDeluxe = {
            titles: ["grammar-syntax", "normalization", "categorization", "general fixes", "template fixes", "+template", "+ internal link(s)", "+ external link(s)", "rem. external link(s)"],
            inputs: ["grammar-syntax", "normalization", "categorization", "general fixes", "template fixes", "+template", "+ internal link(s)", "+ external link(s)", "rem. external link(s)"],
            addToSummary: function (str) {
                var summaryField = document.getElementById('wpSummary');
                if (summaryField) {
                    summaryField.value = summaryField.value ? summaryField.value + '; ' + str : str;
                }
                return false;
            }
        };

        window.resumeDeluxe = resumeDeluxe;

        var DeluxeSummary = function() {
            var summaryLabel = document.getElementById('wpSummaryLabel');
            var summaryField = document.getElementById('wpSummary');

            if (summaryLabel && summaryField) {
                // Prevent duplicate bars by checking for an existing one
                if (summaryLabel.querySelector('.predefined-summaries-bar')) {
                    return;
                }

                // Create container with a unique class name
                var container = document.createElement('span');
                container.className = 'predefined-summaries-bar';
                container.innerHTML = '<b>Predefined summaries</b>: ';

                resumeDeluxe.titles.forEach((title, index) => {
                    if (index > 0) {
                        container.appendChild(document.createTextNode(' / '));
                    }

                    var link = document.createElement('a');
                    link.href = '#';
                    link.className = 'sumLink';
                    link.title = 'Add to edit summary';
                    link.textContent = title;
                    link.addEventListener('click', function (event) {
                        event.preventDefault();
                        resumeDeluxe.addToSummary(resumeDeluxe.inputs[index]);
                    });

                    container.appendChild(link);
                });

                container.appendChild(document.createElement('br'));
                summaryLabel.prepend(container);

                // Adjust width as in original script
                summaryField.style.width = '95%';
            }
        };

        mw.hook('wikipage.content').add(DeluxeSummary);
    }
});

// ANCHOR: Lingo custom script that performs three tasks: 1) Disables the rendering of tooltips of a given acronym within the article that defines it ("RFC" on the "Request For Comments" article); 2) Disables Lingo entirely in arbitrarily defined Namespaces from the frontend; 3) Disables Lingo functionality in specific containers using CSS classes (which is supplemented by visual removal via Common.css)
    mw.loader.using('mediawiki.util').then(function() {
    $(function() {
    // List namespaces where Lingo should be disabled (adjust freely)
    var blockedNamespaces = ['Template', 'Category', 'Module'];
    var currentNS = mw.config.get('wgCanonicalNamespace');
    
    // List CSS classes within which Lingo should be disabled
    var blockedClasses = ['template-table', 'country-hub-infobox', 'campaign-instructions'];
    
    if (blockedNamespaces.indexOf(currentNS) !== -1) {
        // Remove any Lingo tooltip markup immediately
        var terms = document.querySelectorAll('.mw-lingo-term');
        terms.forEach(function(term) {
            term.parentNode.replaceChild(document.createTextNode(term.textContent), term);
        });
        console.log("Lingo processing is blocked in the '" + currentNS + "' namespace.");
        return; // Stop further processing
    }
    
    // Process CSS class-based blocking
    blockedClasses.forEach(function(className) {
        var terms = document.querySelectorAll('.' + className + ' .mw-lingo-term');
        var count = terms.length;
        
        if (count > 0) {
            terms.forEach(function(term) {
                term.parentNode.replaceChild(document.createTextNode(term.textContent), term);
            });
            console.log("Removed " + count + " Lingo tooltip(s) from ." + className + " container(s).");
        }
    });

    // Decent enough solution for Lingo not to render tooltips of an acronym on the page that defines it
    var rawTitle = mw.config.get('wgTitle').replace(/_/g, ' ').trim();
    var acronym = null;

    // Case 1: Title written as "Long Form (ACRONYM)"
    var titleAcronymMatch = rawTitle.match(/^(.+?)\s+\(([A-Z]{2,})\)$/);
    if (titleAcronymMatch) {
        acronym = titleAcronymMatch[2];
        console.log("Using acronym from title parenthetical: " + acronym);
    }
    // Case 2: Title is exactly an acronym (all uppercase)
    else if (/^[A-Z]+$/.test(rawTitle)) {
        acronym = rawTitle;
        console.log("Using pure acronym from title: " + acronym);
    }
    // Otherwise, compute fallback from title’s longform and compare with extracted acronym
    else {
        // Remove any trailing parenthetical from title, e.g. "Request For Comments (foo)" becomes "Request For Comments"
        var longForm = rawTitle.replace(/\s*\(.*\)\s*$/, '');
        var fallback = longForm.split(/\s+/).map(function(word) {
            return word.charAt(0).toUpperCase();
        }).join('');
        
        // Attempt to extract an acronym from the first paragraph
        var firstParagraph = $('#mw-content-text p').first().text();
        var parenMatch = firstParagraph.match(/\(([A-Z]{2,})\)/);
        if (parenMatch) {
            var extracted = parenMatch[1];
            console.log("Extracted acronym from first paragraph: " + extracted);
            if (extracted === fallback) {
                acronym = extracted;
                console.log("Acronym matches fallback computed from title: " + acronym);
            } else {
                console.log("Extracted acronym (" + extracted + ") does not match computed fallback (" + fallback + "); not proceeding with tooltip removal");
                return;
            }
        } else {
            console.log("No acronym found in first paragraph; not proceeding with tooltip removal");
            return;
        }
    }

    console.log("Assuming definition page for acronym: " + acronym);
    // Remove any Lingo tooltip markup for elements whose visible text exactly matches the acronym.
    var all = document.querySelectorAll('.mw-lingo-term');
    var matching = Array.prototype.filter.call(all, function(term) {
        return term.textContent.trim() === acronym;
    });
    console.log("Found " + matching.length + " element(s) with visible text '" + acronym + "'");
    
    matching.forEach(function(term) {
        term.parentNode.replaceChild(document.createTextNode(term.textContent), term);
    });
    
    console.log("Finished processing Lingo tooltips for acronym: " + acronym);
    });
});

// ANCHOR: Set "File" Categories intelligently based on file type
mw.loader.using(['mediawiki.util'], function() {
    (function() {
        // Only run on upload pages
        var page = mw.config.get('wgCanonicalSpecialPageName');
        if (
            window.FileCategoryLoaded ||
            !(/Upload|MultipleUpload/g.test(page))
        ) {
            return;
        }
        window.FileCategoryLoaded = true;
        
        // Define file type mapping
        var categoryMapping = {
            // Images
            'jpg': 'Images',
            'jpeg': 'Images',
            'png': 'Images', 
            'gif': 'Images',
            'svg': 'Images',
            'webp': 'Images',
            'bmp': 'Images',
            'tiff': 'Images',
            
            // Documents
            'pdf': 'Documents',
            'doc': 'Documents',
            'docx': 'Documents',
            'ppt': 'Documents',
            'pptx': 'Documents',
            'xls': 'Documents',
            'xlsx': 'Documents',
            'txt': 'Documents',
            'rtf': 'Documents',
            'odt': 'Documents',
            'ods': 'Documents',
            'odp': 'Documents',
            
            // Archives
            'zip': 'Archives',
            'rar': 'Archives',
            'tar': 'Archives',
            'gz': 'Archives',
            '7z': 'Archives',
            
            // Audio
            'mp3': 'Audio',
            'wav': 'Audio',
            'ogg': 'Audio',
            'flac': 'Audio',
            
            // Video
            'mp4': 'Video',
            'webm': 'Video',
            'avi': 'Video',
            'mov': 'Video',
            'mkv': 'Video',
            
            // Default fallback
            'default': 'Files'
        };
        
        // Extract clean extension from filename
        function getFileExtension(filename) {
            if (!filename) return '';
            
            // Remove path (handle both Windows and Unix paths)
            var cleanName = filename.split('\\').pop().split('/').pop();
            
            // Extract extension
            var parts = cleanName.split('.');
            if (parts.length <= 1) return '';
            
            return parts.pop().toLowerCase();
        }
        
        // Apply category based on file extension
        function applyCategoryFromExtension(filename) {
            var $descField = $('#wpUploadDescription');
            if (!$descField.length || !filename) return;
            
            var extension = getFileExtension(filename);
            if (!extension) return;
            
            var category = categoryMapping[extension] || categoryMapping['default'];
            
            // Update the description field, preserving any existing content
            var currentDesc = $descField.val() || '';
            
            // Remove any existing category we might have added before
            Object.values(categoryMapping).forEach(function(catName) {
                var catRegex = new RegExp('\\[\\[Category:' + catName + '\\]\\]', 'g');
                currentDesc = currentDesc.replace(catRegex, '');
            });
            
            // Trim any whitespace created by removing categories
            currentDesc = currentDesc.trim();
            
            // Add our new category
            var newDesc = currentDesc;
            if (newDesc) {
                // Add to existing content
                newDesc += '\n\n[[Category:' + category + ']]';
            } else {
                // No content yet
                newDesc = '[[Category:' + category + ']]';
            }
            
            $descField.val(newDesc);
            console.log('FileCategory: Applied category ' + category + ' based on extension .' + extension);
        }
        
        // Set up event listeners
        function setupEventListeners() {
            var $fileInput = $('#wpUploadFile');
            
            // Skip if we can't find the file input
            if (!$fileInput.length) return;
            
            // Listen for file selection changes
            $fileInput.on('change', function() {
                var filename = $(this).val();
                if (filename) {
                    applyCategoryFromExtension(filename);
                }
            });
            
            // Also hook into MediaWiki's upload events if available
            if (typeof mw.hook !== 'undefined') {
                mw.hook('uploadform.fileSelected').add(function(data) {
                    if (data && data.filename) {
                        applyCategoryFromExtension(data.filename);
                    }
                });
            }
            
            // For AJAX-based uploads that might update the file input after page load
            var observer = new MutationObserver(function(mutations) {
                mutations.forEach(function(mutation) {
                    if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
                        var filename = $fileInput.val();
                        if (filename) {
                            applyCategoryFromExtension(filename);
                        }
                    }
                });
            });
            
            // Observe the file input for value changes
            observer.observe($fileInput[0], { attributes: true });
        }
        
        // Initialize when DOM is ready
        $(setupEventListeners);
    })();
});

// ANCHOR: Responsive expanding menus 
/* REVIEW: Codex */
var menuState = {
    initialized: false,
    isCurrentlyMobile: false,
    originalTopRow: null,
    originalPrimaryMenu: null,
    originalSecondaryMenu: null,
    accordionState: {},
    hasLoggedSetupUnnecessary: false  // Track if we've already logged setup unnecessary message
};

// Media query for mobile breakpoint
var mobileMediaQuery = window.matchMedia("(max-width: 41rem)");

// Main setup function for responsive menu behavior: once per page load
function setupResponsiveMenus() {
    var $topRow = $('.icannwiki-top-row');
    
    // Only proceed if we have the top row
    if ($topRow.length === 0) {
        // Only log if we haven't already logged this message
        if (!menuState.hasLoggedSetupUnnecessary) {
            console.log("Menu setup unnecessary");
            menuState.hasLoggedSetupUnnecessary = true;
        }
        return;
    }
    
    // Capture the original DOM state and save it
    if (!menuState.initialized) {
        // Store original DOM state references (clone deeply to preserve all attributes and content)
        menuState.originalTopRow = $topRow.clone(true, true);
        menuState.originalPrimaryMenu = $topRow.find('.icannwiki-menu-container.primary').clone(true, true);
        menuState.originalSecondaryMenu = $topRow.find('.icannwiki-menu-container.secondary').clone(true, true);
        menuState.initialized = true;
        console.log("Original menu state captured");
    }
    
    // Setup viewport change listener for future screen size changes
    try {
        mobileMediaQuery.removeEventListener('change', handleViewportChange);
    } catch (e) {
        // Ignore any errors from trying to remove a non-existent listener
    }
    
    // Setup listener for future viewport changes
    mobileMediaQuery.addEventListener('change', handleViewportChange);
    
    // Initial layout setup based on current viewport at page load
    setupInitialLayout(mobileMediaQuery.matches);
}

// Initial layout setup function: once per page load
function setupInitialLayout(isMobile) {
    menuState.isCurrentlyMobile = isMobile;
    
    if (isMobile) {
        // First create the mobile accordions directly and completely
        initMobileLayoutImmediately();
    } 
    // Desktop layout is the default, no need to do anything
}

// Handle viewport size changes after initial load
function handleViewportChange(mediaQuery) {
    if (mediaQuery.matches && !menuState.isCurrentlyMobile) {
        // Switching desktop -> mobile
        menuState.isCurrentlyMobile = true;
        initMobileLayoutImmediately();
    } else if (!mediaQuery.matches && menuState.isCurrentlyMobile) {
        // Switching mobile -> desktop
        menuState.isCurrentlyMobile = false;
        restoreDesktopLayout();
    }
    // If already in the correct layout mode, do nothing to avoid flashing
}

// Create mobile layout immediately without any timeouts
function initMobileLayoutImmediately() {
    console.log("Setting up mobile accordions");
    
    // Get reference to the top row
    var $topRow = $('.icannwiki-top-row');
    
    // If the top row is already set up as accordions, don't recreate them
    if ($topRow.find('.icannwiki-accordion-section').length > 0) {
        console.log("Accordions already exist, no need to reinitialize");
        
        // Make sure the top row is visible 
        if (!$topRow.hasClass('mobile-ready')) {
            $topRow.addClass('mobile-ready');
        }
        return;
    }
    
    // Ensure we have an initialized state
    if (!menuState.initialized) {
        console.error("Menu state not initialized, cannot create accordions");
        return;
    }
    
    // Store any existing accordion open/closed states
    $('.icannwiki-accordion-header').each(function() {
        var $header = $(this);
        var isActive = $header.hasClass('active');
        var text = $header.text().trim();
        menuState.accordionState[text] = isActive;
    });
    
    // Get fresh copies of the original menu elements
    var $primaryMenu = menuState.originalPrimaryMenu.clone(true, true);
    var $secondaryMenu = menuState.originalSecondaryMenu.clone(true, true);
    
    // Clear the top row in preparation for adding accordions
    $topRow.empty();
    
    // Build complete accordion structure with content in HTML
    var accordionHTML = 
        '<div class="icannwiki-accordion-section">' +
            '<button type="button" class="icannwiki-accordion-header primary" aria-expanded="false">ICANN</button>' +
            '<div class="icannwiki-accordion-content" aria-hidden="true"></div>' +
        '</div>' +
        '<div class="icannwiki-accordion-section">' +
            '<button type="button" class="icannwiki-accordion-header secondary" aria-expanded="false">Internet Governance</button>' +
            '<div class="icannwiki-accordion-content" aria-hidden="true"></div>' +
        '</div>';
    
    // Append the full accordion structure at once
    $topRow.append(accordionHTML);
    
    // Insert the original menu content into the accordion containers
    $topRow.find('.icannwiki-accordion-section:eq(0) .icannwiki-accordion-content').append($primaryMenu);
    $topRow.find('.icannwiki-accordion-section:eq(1) .icannwiki-accordion-content').append($secondaryMenu);
    
    // Add click handlers for accordion toggles
    $('.icannwiki-accordion-header').on('click', function() {
        var $header = $(this);
        var $content = $header.next('.icannwiki-accordion-content');
        var isOpen = $header.hasClass('active');
        
        // Store the state for persistence across viewport changes
        menuState.accordionState[$header.text().trim()] = !isOpen;
        
        // Toggle active state
        $header.toggleClass('active');
        $content.toggleClass('active');
        
        // Update ARIA attributes
        $header.attr('aria-expanded', !isOpen);
        $content.attr('aria-hidden', isOpen);
    });
    
    // Restore any previously open accordions
    $('.icannwiki-accordion-header').each(function() {
        var $header = $(this);
        var text = $header.text().trim();
        
        if (menuState.accordionState[text]) {
            $header.addClass('active');
            $header.next('.icannwiki-accordion-content').addClass('active');
            $header.attr('aria-expanded', 'true');
            $header.next('.icannwiki-accordion-content').attr('aria-hidden', 'false');
        }
    });
    
    // Make the top row visible now that it's fully built
    $topRow.addClass('mobile-ready');
}

// Function to restore desktop layout
function restoreDesktopLayout() {
    console.log("Restoring desktop layout");
    
    var $topRow = $('.icannwiki-top-row');
    
    // If no accordions present, assume already in desktop layout
    if ($topRow.find('.icannwiki-accordion-section').length === 0) {
        console.log("Already in desktop layout");
        return;
    }
    
    // Get fresh copies of the original menus
    var $primaryMenu = menuState.originalPrimaryMenu.clone(true, true);
    var $secondaryMenu = menuState.originalSecondaryMenu.clone(true, true);
    
    // Clear top row and append original menus in one operation
    $topRow.empty()
           .append($primaryMenu)
           .append($secondaryMenu)
           .removeClass('mobile-ready');
}
// Initialize on page load
setupResponsiveMenus();

// ANCHOR: ElementPortraitCarousel dynamics
$(function() {
    // Only execute if there are any ".person-portrait-carousel" on the page
    if ($('.person-portrait-carousel').length === 0) return;
    
    console.log('Initializing person template carousel');
    
    // Initialize all carousels on the page
    $('.carousel-container').each(function() {
        var $container = $(this);
        var $items = $container.find('.carousel-item');
        
        // Skip if no items
        if ($items.length === 0) return;
        
        // Wait for images to load
        $items.find('img').on('load', function() {
            $(this).data('loaded', true);
        }).each(function() {
            // If already complete, trigger load event
            if (this.complete) {
                $(this).trigger('load');
            }
        });
        
        // Initial positioning of carousel items
        function initializeCarousel() {
            // Show first image, position others
            $items.eq(0).addClass('carousel-visible').removeClass('carousel-hidden');
            
            // Logic for exactly 2 images (orbital layout)
            if ($items.length === 2) {
                $items.eq(0).addClass('carousel-visible carousel-orbital-1').removeClass('carousel-hidden carousel-orbital-2 carousel-left carousel-right');
                $items.eq(1).addClass('carousel-hidden carousel-orbital-2').removeClass('carousel-visible carousel-orbital-1 carousel-left carousel-right');
            } 
            // Logic for 3+ images (card shuffle)
            else if ($items.length > 2) {
                $items.eq(1).addClass('carousel-right').removeClass('carousel-left carousel-visible');
                $items.eq($items.length - 1).addClass('carousel-left').removeClass('carousel-right carousel-visible');
            }
            
            // Let images dimension within the fixed container
            $container.find('.carousel-item img').css({
                'max-height': '100%',
                'max-width': '100%',
                'object-fit': 'contain'
            });
        }
        
        // Initialize carousel after a delay to ensure proper rendering
        setTimeout(initializeCarousel, 100);
    });
    
// Click handler for buttons
$('.carousel-next').on('click', function() {
    var $container = $(this).closest('.carousel-container');
    var $items = $container.find('.carousel-item');
    var $current = $container.find('.carousel-visible');
    var currentIndex = parseInt($current.data('index'));
    var nextIndex = (currentIndex % $items.length) + 1;
    var $next = $container.find('.carousel-item[data-index="' + nextIndex + '"]');
    
    // Check if there are exactly 2 images
    if ($items.length === 2) {
        // Clear any existing classes and animations
        $items.removeClass('carousel-left carousel-right orbital-animating-forward orbital-animating-backward');
        
        // Apply the animation class to create orbital motion
        $current.addClass('orbital-animating-forward');
        $next.addClass('orbital-animating-backward');
        
        // After animation completes, update the final state classes
        setTimeout(function() {
            $current.removeClass('carousel-visible carousel-orbital-1 orbital-animating-forward')
                   .addClass('carousel-hidden carousel-orbital-2');
            $next.removeClass('carousel-hidden carousel-orbital-2 orbital-animating-backward')
                 .addClass('carousel-visible carousel-orbital-1');
        }, 600); // Match CSS animation duration (0.6s)
    } else {
        // Logic for 3+ images
        // Remove position classes
        $items.removeClass('carousel-left carousel-right');
        
        // Removals and additions
        $current.removeClass('carousel-visible').addClass('carousel-hidden carousel-left');
        $next.removeClass('carousel-hidden carousel-left carousel-right').addClass('carousel-visible');
        
        // Set the item after next to right
        var afterNextIndex = (nextIndex % $items.length) + 1;
        if (afterNextIndex > $items.length) afterNextIndex = 1;
        var $afterNext = $container.find('.carousel-item[data-index="' + afterNextIndex + '"]');
        if ($afterNext.length && !$afterNext.is($next)) {
            $afterNext.removeClass('carousel-visible carousel-left').addClass('carousel-hidden carousel-right');
        }
    }
});

// Click handler for the previous button for 2-image case
    $('.carousel-prev').on('click', function() {
        // Get container and items
        var $container = $(this).closest('.carousel-container');
        var $items = $container.find('.carousel-item');
        var $current = $container.find('.carousel-visible');
        
        // Calculate previous index
        var currentIndex = parseInt($current.data('index'));
        var prevIndex = currentIndex - 1;
        if (prevIndex < 1) prevIndex = $items.length;
        var $prev = $container.find('.carousel-item[data-index="' + prevIndex + '"]');
        
        // Check if we have exactly 2 images: use reversed logic compared to next button
        if ($items.length === 2) {
            // Clear any existing classes and animations
            $items.removeClass('carousel-left carousel-right orbital-animating-forward orbital-animating-backward');
            
            // Apply correct animation classes for leftward movement
            $current.addClass('orbital-animating-forward'); // Front item moves back
            $prev.addClass('orbital-animating-backward');  // Back item moves forward
            
            // Update final state and remove correct animation classes
            var updateFinalState = function() {
                $current.removeClass('carousel-visible carousel-orbital-1 orbital-animating-forward') // Remove forward animation class
                       .addClass('carousel-hidden carousel-orbital-2');
                $prev.removeClass('carousel-hidden carousel-orbital-2 orbital-animating-backward') // Remove backward animation class
                     .addClass('carousel-visible carousel-orbital-1');
            };
            
            // Match CSS animation duration (0.6s)
            setTimeout(updateFinalState, 600);
        } else {
            // Logic for 3+ images (card shuffle)
            // Remove position classes
            $items.removeClass('carousel-left carousel-right');
            
            // Removals and additions
            $current.removeClass('carousel-visible').addClass('carousel-hidden carousel-right');
            $prev.removeClass('carousel-hidden carousel-left carousel-right').addClass('carousel-visible');
            
            // Set the item before prev to left
            var beforePrevIndex = prevIndex - 1;
            if (beforePrevIndex < 1) beforePrevIndex = $items.length;
            var $beforePrev = $container.find('.carousel-item[data-index="' + beforePrevIndex + '"]');
            if ($beforePrev.length && !$beforePrev.is($prev)) {
                $beforePrev.removeClass('carousel-visible carousel-right').addClass('carousel-hidden carousel-left');
            }
        }
    });
    
    // Touch swipe support for mobile users
    $('.carousel-container').each(function() {
        var $container = $(this);
        var startX, startY, endX, endY;
        var MIN_SWIPE_DISTANCE = 50; // Minimum distance for a swipe to be registered
        var MAX_VERTICAL_DISTANCE = 50; // Maximum vertical movement allowed for horizontal swipe

        // Touch start: record initial position
        $container.on('touchstart', function(e) {
            // Store initial touch coordinates
            startX = e.originalEvent.touches[0].clientX;
            startY = e.originalEvent.touches[0].clientY;
        });

        // Touch end: determine swipe direction and trigger appropriate action
        $container.on('touchend', function(e) {
            // Get final touch coordinates
            endX = e.originalEvent.changedTouches[0].clientX;
            endY = e.originalEvent.changedTouches[0].clientY;
            
            // Calculate horizontal and vertical distance
            var horizontalDistance = endX - startX;
            var verticalDistance = Math.abs(endY - startY);
            
            // Only register as swipe if horizontal movement is significant and vertical movement is limited
            if (Math.abs(horizontalDistance) >= MIN_SWIPE_DISTANCE && verticalDistance <= MAX_VERTICAL_DISTANCE) {
                // Prevent default behavior if it's a horizontal swipe
                e.preventDefault();
                
                if (horizontalDistance > 0) {
                    // Swipe right: go to previous slide
                    $container.find('.carousel-prev').trigger('click');
                } else {
                    // Swipe left: go to next slide
                    $container.find('.carousel-next').trigger('click');
                }
            }
        });
    });
    
    console.log('Template:Person carousel initialization successful');
});
});

// ANCHOR: ElementNavigation buttons entirely clickable
$(function() {
    // Only run this if we find navigation elements
    if ($('.element-navigation-prev, .element-navigation-next').length === 0) return;
    
    // Process each navigation button
    $('.element-navigation-prev, .element-navigation-next').each(function() {
        var $button = $(this);
        var $link = $button.find('a').first();
        
        // Only proceed if there's a link inside the button
        if ($link.length === 0) return;
        
        // Store the link URL
        var linkHref = $link.attr('href');
        
        // Make the entire button clickable
        $button.css('cursor', 'pointer');
        
        // Add click handler to the button
        $button.on('click', function(e) {
            // Don't trigger this handler if the actual link was clicked
            if (e.target === $link[0] || $.contains($link[0], e.target)) return;
            
            // Navigate to the link destination
            window.location.href = linkHref;
        });
    });
    
    console.log('Navigation buttons enhanced for full clickability');
});

// ANCHOR: "Back to Top" button
;(function($, mw) {
    'use strict';

    // Prevent double-loading
    if (window.ICWBackToTopLoaded) {
        return;
    }
    window.ICWBackToTopLoaded = true;

    // Configuration
    const buttonStartThreshold = window.innerHeight; // Show button after scrolling one viewport height
    const scrollAnimationSpeed = 600; // Milliseconds for scroll to top animation
    const buttonFadeSpeed = 300;   // Milliseconds for fade in/out

    // Create the button element
    var $button = $('<a>', {
        href: '#',
        id: 'icw-back-to-top',
        title: 'Back to top',
        role: 'button',
        html: '&uarr;' // REVIEW: Unicode UPWARDS ARROW
    }).appendTo(document.body);

    // If JS runs before CSS has hidden it, this ensures it starts hidden
    $button.hide(); 

    // Scroll event handler (throttled)
    var scrollTimeout;
    $(window).on('scroll.icwBackToTop', function() {
        if (scrollTimeout) {
            clearTimeout(scrollTimeout);
        }
        scrollTimeout = setTimeout(function() {
            if ($(window).scrollTop() > buttonStartThreshold) {
                $button.fadeIn(buttonFadeSpeed);
            } else {
                $button.fadeOut(buttonFadeSpeed);
            }
        }, 100); // Throttle scroll checks to every 100ms
    });

    // Click event handler
    $button.on('click.icwBackToTop', function(e) {
        e.preventDefault();
        $('html, body').animate({ scrollTop: 0 }, scrollAnimationSpeed);
    });

    console.log('Back to Top button initialized');

})(jQuery, mediaWiki);

// ANCHOR: Hero Page Creator Widget
mw.loader.using(['mediawiki.api', 'mediawiki.util'], function() {
    $(function() {
        // Only run if the hero page creator container exists
        if ($('#hero-page-creator').length === 0) return;

        console.log('Initializing page creator widget');

        // Build the simple HTML form
        var formHtml =
            '<div class="icannwiki-search-unifier">' +
                '<div class="mw-inputbox-element">' +
                    '<input type="text" id="hero-page-name" class="searchboxInput" placeholder="Page name">' +
                    '<select id="hero-template-type" class="searchboxInput" disabled>' +
                        '<option value="">Loading...</option>' +
                    '</select>' +
                    '<button id="hero-create-btn" class="mw-ui-button mw-ui-progressive" disabled>Create</button>' +
                '</div>' +
            '</div>';
        
        $('#hero-page-creator').html(formHtml);

        // Fetch available templates from the Lua module
        var api = new mw.Api();
        api.parse('{{#invoke:TemplateStarter|listTemplates}}')
            .done(function(html) {
                var tempDiv = document.createElement('div');
                tempDiv.innerHTML = html;
                var templateListText = tempDiv.textContent || tempDiv.innerText || '';
                
                var availableTemplates = templateListText.split(',').map(function(t) {
                    return t.trim().split('\n')[0].trim();
                }).filter(function(t) {
                    return t.length > 0 && t.match(/^[a-zA-Z][a-zA-Z0-9_\-\s\(\)]*$/);
                });
                
                var $select = $('#hero-template-type');
                $select.empty().append('<option value="">Select a subject (template)</option>');
                
                availableTemplates.forEach(function(template) {
                    $select.append('<option value="' + template + '">' + template + '</option>');
                });
                
                $select.prop('disabled', false);
            })
            .fail(function() {
                $('#hero-template-type').empty().append('<option value="">Error</option>');
            });

        // Enable/disable create button
        function updateHeroCreateButton() {
            var pageName = $('#hero-page-name').val().trim();
            var templateType = $('#hero-template-type').val();
            $('#hero-create-btn').prop('disabled', !pageName || !templateType);
        }

        $('#hero-page-name, #hero-template-type').on('input change', updateHeroCreateButton);

        // Handle create button click
        $('#hero-create-btn').on('click', function() {
            var pageName = $('#hero-page-name').val().trim();
            var templateType = $('#hero-template-type').val();

            if (!pageName || !templateType) return;

            $(this).prop('disabled', true).text('Creating...');

            api.parse('{{#invoke:TemplateStarter|preload|' + templateType + '}}')
                .done(function(html) {
                    var tempDiv = document.createElement('div');
                    tempDiv.innerHTML = html;
                    var templateContent = tempDiv.textContent || tempDiv.innerText || '';
                    
                    // Process placeholders using the same logic as Create a Page
                    var formData = {}; // Hero widget has no form fields
                    templateContent = processPlaceholders(templateContent, pageName, formData);

                    api.create(pageName, { summary: 'Creating new ' + templateType + ' page' }, templateContent)
                        .done(function() {
                            window.location.href = mw.util.getUrl(pageName, { veaction: 'edit' });
                        })
                        .fail(function(code) {
                            if (code === 'articleexists') {
                                if (confirm('Page already exists. Do you want to edit it?')) {
                                    window.location.href = mw.util.getUrl(pageName, { action: 'edit' });
                                }
                            } else {
                                alert('Error creating page: ' + code);
                            }
                            $('#hero-create-btn').prop('disabled', false).text('Create');
                        });
                })
                .fail(function() {
                    alert('Error loading template content.');
                    $('#hero-create-btn').prop('disabled', false).text('Create');
                });
        });
    });
});

// ANCHOR: Create a Page: Dynamic generation with pre-filled templates
mw.loader.using(['mediawiki.api', 'mediawiki.util'], function() {
    $(function() {
        // Only run on pages that have the template creator container
        if ($('#template-creator-container').length === 0) return;
        
        console.log('Initializing TemplateStarter');
        
        // Build the initial HTML with loading state
        var formHtml =
            '<div class="template-creator-main-layout">' +
                '<div class="template-creator-form-container">' +
                    '<h3>Create New Page</h3>' +
                    '<div class="form-group">' +
                        '<label for="template-type">Template Type:</label>' +
                        '<select id="template-type" class="form-control" disabled>' +
                            '<option value="">Loading templates...</option>' +
                        '</select>' +
                    '</div>' +
                    '<div class="form-group">' +
                        '<label for="page-name">Page Name:</label>' +
                        '<input type="text" id="page-name" class="form-control" placeholder="Enter page name...">' +
                    '</div>' +
                    '<div id="dynamic-fields-container"></div>' +
                    '<button id="create-page-btn" class="mw-ui-button mw-ui-progressive" disabled>Create Page</button>' +
                '</div>' +
                '<div id="template-preview" class="template-creator-preview-container" style="display:none;">' +
                    '<h4>Preview:</h4>' +
                    '<pre id="template-preview-content"></pre>' +
                '</div>' +
            '</div>';
        
        // Insert the form
        $('#template-creator-container').html(formHtml);
        
        // Fetch available templates dynamically from Lua module
        var api = new mw.Api();
        api.parse('{{#invoke:TemplateStarter|listTemplates}}')
            .done(function(html) {
                // Extract text content from the parsed HTML
                var tempDiv = document.createElement('div');
                tempDiv.innerHTML = html;
                var templateListText = tempDiv.textContent || tempDiv.innerText || '';
                
                // Parse the comma-separated list and clean each template name
                var availableTemplates = templateListText.split(',').map(function(t) {
                    // Clean the template name: remove newlines, extra whitespace, and any descriptions
                    var cleaned = t.trim().split('\n')[0].trim();
                    return cleaned;
                }).filter(function(t) {
                    return t.length > 0 && t.match(/^[a-zA-Z][a-zA-Z0-9_\-\s\(\)]*$/);
                });
                
                // Populate the dropdown
                var $select = $('#template-type');
                $select.empty().append('<option value="">Select a template...</option>');
                
                availableTemplates.forEach(function(template) {
                    $select.append('<option value="' + template + '">' + template + '</option>');
                });
                
                // Enable the dropdown
                $select.prop('disabled', false);
                
                console.log('Loaded templates dynamically:', availableTemplates);
            })
            .fail(function(error) {
                console.error('Failed to load templates dynamically:', error);
                
                // Show error state instead of fallback
                var $select = $('#template-type');
                $select.empty().append('<option value="">Error loading templates</option>');
                
                // Keep dropdown disabled on error
                $select.prop('disabled', true);
            });
        
        // Enable/disable create button based on form completion
        function updateCreateButton() {
            var templateType = $('#template-type').val();
            var pageName = $('#page-name').val().trim();
            $('#create-page-btn').prop('disabled', !templateType || !pageName);
        }
        
        // Update button state when inputs change
        $('#template-type, #page-name').on('change input', updateCreateButton);
        
        // Preview template when type is selected
        $('#template-type').on('change', function() {
            var templateType = $(this).val();
            if (!templateType) {
                $('#template-preview').hide();
                $('#dynamic-fields-container').empty();
                return;
            }
            
            // Load dynamic fields for this template type
            loadDynamicFields(templateType);
            updatePreview();
        });
        
        // Update preview when page name changes
        $('#page-name').on('input', function() {
            var templateType = $('#template-type').val();
            if (templateType) {
                updatePreview();
            }
        });
        
        // Function to update the preview with processed placeholders
        function updatePreview() {
            var templateType = $('#template-type').val();
            var pageName = $('#page-name').val().trim();
            
            if (!templateType) return;
            
            // Call the Lua module to get template structure
            var api = new mw.Api();
            api.parse('{{#invoke:TemplateStarter|preload|' + templateType + '}}')
                .done(function(html) {
                    // Extract text content from the parsed HTML
                    var tempDiv = document.createElement('div');
                    tempDiv.innerHTML = html;
                    var templateContent = tempDiv.textContent || tempDiv.innerText || '';
                    
                    // Collect form data and process placeholders
                    var formData = collectFormData();
                    templateContent = processPlaceholders(templateContent, pageName, formData);
                    
                    $('#template-preview-content').text(templateContent);
                    $('#template-preview').show();
                })
                .fail(function(error) {
                    console.error('Failed to preview template:', error);
                    $('#template-preview-content').text('Error loading template preview');
                    $('#template-preview').show();
                });
        }
        
        // Function to load dynamic fields for a template type
        function loadDynamicFields(templateType) {
            console.log('Loading dynamic fields for template:', templateType);
            
            var api = new mw.Api();
            api.parse('{{#invoke:TemplateStarter|getCreatorFieldDefinitionsJSON|' + templateType + '}}')
                .done(function(html) {
                    // Extract JSON content from the parsed HTML
                    var tempDiv = document.createElement('div');
                    tempDiv.innerHTML = html;
                    var jsonText = tempDiv.textContent || tempDiv.innerText || '';
                    
                    try {
                        var fieldDefinitions = JSON.parse(jsonText);
                        console.log('Field definitions loaded:', fieldDefinitions);
                        
                        // Generate form fields
                        generateDynamicFields(fieldDefinitions);
                    } catch (e) {
                        console.error('Failed to parse field definitions JSON:', e, jsonText);
                        $('#dynamic-fields-container').html('<p style="color: red;">Error loading field definitions</p>');
                    }
                })
                .fail(function(error) {
                    console.error('Failed to load field definitions:', error);
                    $('#dynamic-fields-container').html('<p style="color: red;">Error loading field definitions</p>');
                });
        }
        
        // Function to generate dynamic form fields
        function generateDynamicFields(fieldDefinitions) {
            var $container = $('#dynamic-fields-container');
            $container.empty();
            
            // Skip PAGE_NAME as it's handled separately
            for (var fieldKey in fieldDefinitions) {
                if (fieldKey === 'PAGE_NAME') continue;
                
                var fieldDef = fieldDefinitions[fieldKey];
                var $formGroup = $('<div class="form-group"></div>');
                
                // Create label
                var $label = $('<label></label>')
                    .attr('for', 'dynamic-field-' + fieldKey)
                    .text(fieldDef.label);
                
                // Create input
                var $input = $('<input type="text" class="form-control dynamic-field">')
                    .attr('id', 'dynamic-field-' + fieldKey)
                    .attr('data-field-key', fieldKey)
                    .attr('placeholder', fieldDef.placeholder);
                
                // Mark required fields
                if (fieldDef.required) {
                    $label.append(' <span style="color: red;">*</span>');
                    $input.attr('required', true);
                }
                
                $formGroup.append($label).append($input);
                $container.append($formGroup);
                
                // Add event listener for preview updates
                $input.on('input', function() {
                    var templateType = $('#template-type').val();
                    if (templateType) {
                        updatePreview();
                    }
                });
            }
            
            // Update create button state when dynamic fields change
            $container.on('input', '.dynamic-field', updateCreateButton);
            
            console.log('Dynamic fields generated');
        }
        
        // Function to collect form data from dynamic fields
        function collectFormData() {
            var formData = {};
            
            // Only collect dynamic field values (PAGE_NAME is handled separately)
            $('.dynamic-field').each(function() {
                var $field = $(this);
                var fieldKey = $field.data('field-key');
                var value = $field.val().trim();
                
                if (value) {
                    formData[fieldKey] = value;
                }
            });
            
            return formData;
        }
        
        // Function to process $VARIABLE$ placeholders
        function processPlaceholders(templateContent, pageName, formData) {
            var processedContent = templateContent;
            
            // Replace $PAGE_NAME$ with the actual page name (always, even if empty)
            processedContent = processedContent.replace(/\$PAGE_NAME\$/g, pageName || '');
            
            // Replace other placeholders with form data (only if values are not empty)
            for (var key in formData) {
                if (key !== 'PAGE_NAME' && formData[key] && formData[key].trim() !== '') {
                    var placeholder = '$' + key + '$';
                    // Properly escape the $ characters for regex
                    var escapedPlaceholder = placeholder.replace(/\$/g, '\\$');
                    processedContent = processedContent.replace(new RegExp(escapedPlaceholder, 'g'), formData[key]);
                }
            }
            
            // Clean up any remaining placeholders that don't have form fields (Hero widget case)
            // Only remove placeholders that are not PAGE_NAME and don't have corresponding form fields
            var availableFields = Object.keys(formData || {});
            availableFields.push('PAGE_NAME'); // Always preserve PAGE_NAME processing
            
            // Find all placeholders in the content
            var placeholderMatches = processedContent.match(/\$[A-Z_]+\$/g) || [];
            
            for (var i = 0; i < placeholderMatches.length; i++) {
                var placeholder = placeholderMatches[i];
                var fieldName = placeholder.replace(/\$/g, '');
                
                // If this placeholder doesn't have a corresponding form field, remove it
                if (availableFields.indexOf(fieldName) === -1) {
                    var escapedPlaceholder = placeholder.replace(/\$/g, '\\$');
                    processedContent = processedContent.replace(new RegExp(escapedPlaceholder, 'g'), '');
                }
            }
            
            // Clean up any resulting grammar issues
            processedContent = processedContent.replace(/\s+/g, ' '); // Multiple spaces to single
            processedContent = processedContent.replace(/is a\s+based/g, 'is based'); // Fix "is a  based"
            processedContent = processedContent.replace(/is a\s+in/g, 'is in'); // Fix "is a  in"
            processedContent = processedContent.replace(/\s+\./g, '.'); // Space before period
            
            return processedContent;
        }
        
        // Handle create button click
        $('#create-page-btn').on('click', function() {
            var templateType = $('#template-type').val();
            var pageName = $('#page-name').val().trim();
            
            if (!templateType || !pageName) {
                alert('Please select a template type and enter a page name');
                return;
            }
            
            // Disable button to prevent double-clicks
            $(this).prop('disabled', true).text('Creating...');
            
            // Get template content via API
            var api = new mw.Api();
            api.parse('{{#invoke:TemplateStarter|preload|' + templateType + '}}')
                .done(function(html) {
                    // Extract text content from the parsed HTML
                    var tempDiv = document.createElement('div');
                    tempDiv.innerHTML = html;
                    var templateContent = tempDiv.textContent || tempDiv.innerText || '';
                    
                    // Process placeholders before creating the page
                    var formData = collectFormData();
                    var processedContent = processPlaceholders(templateContent, pageName, formData);
                    
                    // Create the page with the processed template content
                    api.create(pageName, {
                        summary: 'Creating new ' + templateType + ' page'
                    }, processedContent)
                    .done(function() {
                        // Redirect to the new page in Visual Edit mode
                        window.location.href = mw.util.getUrl(pageName, { veaction: 'edit' });
                    })
                    .fail(function(code, error) {
                        if (code === 'articleexists') {
                            // Page already exists, open it for editing with the template
                            if (confirm('Page already exists. Do you want to edit it and add the template?')) {
                                window.location.href = mw.util.getUrl(pageName, { veaction: 'edit' });
                            }
                        } else {
                            console.error('Failed to create page:', code, error);
                            alert('Error creating page: ' + (error.error ? error.error.info : code));
                        }
                        $('#create-page-btn').prop('disabled', false).text('Create Page');
                    });
                })
                .fail(function(error) {
                    console.error('Failed to get template:', error);
                    alert('Error loading template. Please try again.');
                    $('#create-page-btn').prop('disabled', false).text('Create Page');
                });
        });
        
        console.log('TemplateStarter initialized');
    });
});

// ANCHOR: Template debug monitor
$(function() {
    // Initialize the template debug monitor
    window.templateDebugMonitor = {
        // Cache to track already seen errors to prevent duplicates
        seenErrors: {},
        
        // Scan for debug information in invisible divs with more efficient error reporting
        scan: function() {
            const html = document.documentElement.innerHTML;
            let found = false;
            
            // Only look for the data attribute format - our unified approach
            const templateErrorDivs = document.querySelectorAll('div[data-template-error]');
            if (templateErrorDivs.length > 0) {
                templateErrorDivs.forEach(div => {
                    // Create a unique ID for this error set
                    const errorCount = parseInt(div.getAttribute('data-error-count') || '0');
                    if (errorCount === 0) return;
                    
                    // Create a fingerprint of the errors to avoid duplicates
                    let errorFingerprint = "";
                    for (let i = 1; i <= errorCount; i++) {
                        const source = div.getAttribute(`data-error-${i}-source`) || '';
                        const msg = div.getAttribute(`data-error-${i}-msg`) || '';
                        errorFingerprint += `${source}:${msg};`;
                    }
                    
                    // Skip if we've already seen this exact set of errors
                    if (this.seenErrors[errorFingerprint]) {
                        return;
                    }
                    
                    // Mark as seen
                    this.seenErrors[errorFingerprint] = true;
                    found = true;
                    
                    // Log to console
                    console.group(`Template Error`);
                    console.warn(`Found ${errorCount} error(s)`);
                    
                    // Look for individual error attributes with our simplified naming
                    for (let i = 1; i <= errorCount; i++) {
                        const source = div.getAttribute(`data-error-${i}-source`);
                        const msg = div.getAttribute(`data-error-${i}-msg`);
                        const details = div.getAttribute(`data-error-${i}-details`);
                        
                        if (source && msg) {
                            console.error(`Error in ${source}: ${msg}`);
                            if (details) {
                                console.info('Details:', details);
                            }
                        }
                    }
                    
                    console.groupEnd();
                });
            }
            
            // Structure errors with minimal format
            const structureErrorDivs = document.querySelectorAll('div[data-structure-error]');
            if (structureErrorDivs.length > 0) {
                structureErrorDivs.forEach(div => {
                    const errorCount = parseInt(div.getAttribute('data-error-count') || '0');
                    if (errorCount === 0) return;
                    
                    // Create fingerprint for deduplication
                    let errorFingerprint = "structure:";
                    Array.from(div.attributes)
                        .filter(attr => attr.name.startsWith('data-error-block-'))
                        .forEach(attr => {
                            errorFingerprint += `${attr.name}:${attr.value};`;
                        });
                    
                    // Skip if already seen
                    if (this.seenErrors[errorFingerprint]) {
                        return;
                    }
                    
                    // Mark as seen
                    this.seenErrors[errorFingerprint] = true;
                    found = true;
                    
                    console.group(`Template Structure Error`);
                    console.warn(`Found ${errorCount} block error(s)`);
                    
                    // Scan all attributes
                    Array.from(div.attributes)
                        .filter(attr => attr.name.startsWith('data-error-block-'))
                        .forEach(attr => {
                            const blockNum = attr.name.replace('data-error-block-', '');
                            console.error(`Error in Block ${blockNum}: ${attr.value}`);
                        });
                    
                    console.groupEnd();
                });
            }
            
            // Look for emergency outputs (minimal attributes and format)
            const emergencyDivs = document.querySelectorAll('div[data-critical="1"]');
            if (emergencyDivs.length > 0) {
                emergencyDivs.forEach(div => {
                    const source = div.getAttribute('data-error-1-source') || 'unknown';
                    const msg = div.getAttribute('data-error-1-msg') || 'Critical error';
                    
                    // Create fingerprint for deduplication
                    const errorFingerprint = `critical:${source}:${msg}`;
                    
                    // Skip if already seen
                    if (this.seenErrors[errorFingerprint]) {
                        return;
                    }
                    
                    // Mark as seen
                    this.seenErrors[errorFingerprint] = true;
                    found = true;
                    
                    console.group(`Critical Template Error`);
                    console.error(`Template crashed in ${source}: ${msg}`);
                    console.groupEnd();
                });
            }
            
            return found;
        },
        
        // Initialize the monitor - with throttling to prevent excessive scanning
        init: function() {
            let scanTimeout = null;
            
            // Define a throttled scan function
            const throttledScan = () => {
                if (scanTimeout) {
                    clearTimeout(scanTimeout);
                }
                scanTimeout = setTimeout(() => {
                    this.scan();
                    scanTimeout = null;
                }, 300); // Throttle to once every 300ms
            };
            
            // Run initial scan after page load
            throttledScan();
            
            // Run when content changes
            mw.hook('wikipage.content').add(throttledScan);
            
            console.log('Template Debug Monitor initialized');
        }
    };
    
    // Initialize the debug monitor
    templateDebugMonitor.init();
    
    // Debug function for achievements
    window.debugAchievements = function() {
        
        // Get page ID
        var pageId = mw.config.get('wgArticleId');
        console.log("Page ID: " + pageId);
        
        // Check for achievement header
        var headerElement = document.querySelector('.achievement-header');
        if (headerElement) {
            console.log("Achievement Header Found:");
            console.log("  Class: " + headerElement.className);
            console.log("  Data ID: " + headerElement.getAttribute('data-achievement-id'));
            console.log("  Data Name: " + headerElement.getAttribute('data-achievement-name'));
            console.log("  Text: " + headerElement.textContent.trim());
        } else {
            console.log("No achievement header found");
        }
        
        // Check for achievement badges
        var badgeElements = document.querySelectorAll('.achievement-badge');
        if (badgeElements.length > 0) {
            console.log("Achievement Badges Found: " + badgeElements.length);
            badgeElements.forEach(function(badge, index) {
                console.log("Badge " + (index + 1) + ":");
                console.log("  Class: " + badge.className);
                console.log("  Data ID: " + badge.getAttribute('data-achievement-id'));
                console.log("  Data Name: " + badge.getAttribute('data-achievement-name'));
            });
        } else {
            console.log("No achievement badges found");
        }
    };
    
    // Run the debug function when the page loads
    setTimeout(function() {
        if (typeof window.debugAchievements === 'function') {
            window.debugAchievements();
        }
    }, 1000);
});