Jump to content

User:Polygnotus/Scripts/WordCount.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// <nowiki>
// Wikipedia Talk Page Participation Analyzer
// Only runs on talk pages and adds participation analysis links to section headers
// This ignores text in links

(function() {
    'use strict';
    
    // Only run on talk pages
    if (!mw.config.get('wgCanonicalNamespace') || 
        !mw.config.get('wgCanonicalNamespace').toLowerCase().includes('talk')) {
        return;
    }
    
    // Function to count words in text (excluding links, timestamps, and other markup)
    function countWords(text) {
        if (!text) return 0;
        
        // Create a temporary element to parse HTML and extract text without signature elements
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = text;
        
        // Remove all signature-related elements
        const elementsToRemove = tempDiv.querySelectorAll(
            'a, ' +                                           // All links
            '.ext-discussiontools-init-timestamplink, ' +    // Timestamp links
            '.ext-discussiontools-init-replylink-buttons, ' + // Reply buttons
            '[data-mw-comment-sig], ' +                      // Signature markers
            '.mw-editsection, ' +                           // Edit section links
            '.ext-discussiontools-init-section-subscribe'    // Subscribe links
        );
        elementsToRemove.forEach(el => el.remove());
        
        // Get plain text content
        let plainText = tempDiv.textContent || tempDiv.innerText || '';
        
        // Remove common timestamp patterns and other signature artifacts
        plainText = plainText
            .replace(/\d{2}:\d{2},\s*\d{1,2}\s+\w+\s+\d{4}\s*\(UTC\)/g, '') // Timestamps like "12:35, 1 September 2025 (UTC)"
            .replace(/\(\s*talk\s*\)/gi, '')                                    // (talk) links
            .replace(/\[\s*reply\s*\]/gi, '')                                   // [reply] buttons
            .replace(/\[\s*edit\s*\]/gi, '')                                    // [edit] buttons
            .replace(/\[\s*subscribe\s*\]/gi, '')                               // [subscribe] buttons
            .replace(/\[\s*unsubscribe\s*\]/gi, '')                             // [unsubscribe] buttons
            .replace(/📌|🔗/g, '')                                               // Emoji icons
            .trim();
        
        // Debug: log what we're counting
        console.log('Text being counted:', JSON.stringify(plainText));
        const words = plainText.split(/\s+/).filter(word => 
            word.length > 0 && 
            word !== '()' &&           // Filter out empty parentheses
            word !== '[]' &&           // Filter out empty brackets
            !/^\(\s*\)$/.test(word) && // Filter out parentheses with only whitespace
            !/^\[\s*\]$/.test(word)    // Filter out brackets with only whitespace
        );
        console.log('Words found:', words);
        console.log('Word count:', words.length);
        
        // Count words in the cleaned text
        return words.length;
    }
    
    // Function to extract username from user page link
    function extractUsername(href) {
        const userMatch = href.match(/\/wiki\/User:([^\/]+)$/);
        if (userMatch) {
            return decodeURIComponent(userMatch[1].replace(/_/g, ' '));
        }
        return null;
    }
    
    // Function to analyze participation in a section
    function analyzeSection(sectionElement) {
        const userStats = {};
        
        // Look for DiscussionTools comment markers
        const commentElements = sectionElement.querySelectorAll('[data-mw-comment-start]');
        
        if (commentElements.length > 0) {
            // DiscussionTools format - analyze each comment
            commentElements.forEach(commentStart => {
                const commentId = commentStart.getAttribute('data-mw-comment-start') || commentStart.id;
                
                // Skip section headers (they start with 'h-'), only process comments (start with 'c-')
                if (commentId.startsWith('h-')) {
                    return;
                }
                
                // Find the corresponding comment end element
                let commentEnd = null;
                const allElements = sectionElement.querySelectorAll('*');
                for (let el of allElements) {
                    if (el.getAttribute('data-mw-comment-end') === commentId) {
                        commentEnd = el;
                        break;
                    }
                }
                
                if (commentEnd) {
                    // Extract comment content between start and end, excluding signature elements
                    let commentContent = '';
                    let currentNode = commentStart.nextSibling;
                    
                    while (currentNode && currentNode !== commentEnd) {
                        if (currentNode.nodeType === Node.TEXT_NODE) {
                            commentContent += currentNode.textContent;
                        } else if (currentNode.nodeType === Node.ELEMENT_NODE) {
                            // Skip signature elements completely
                            if (!currentNode.classList.contains('ext-discussiontools-init-timestamplink') &&
                                !currentNode.hasAttribute('data-mw-comment-sig') &&
                                !currentNode.classList.contains('ext-discussiontools-init-replylink-buttons') &&
                                currentNode.tagName !== 'A') {  // Skip all anchor tags
                                commentContent += currentNode.textContent;
                            }
                        }
                        currentNode = currentNode.nextSibling;
                    }
                    
                    // Clean the extracted content further
                    commentContent = commentContent
                        .replace(/\d{2}:\d{2},\s*\d{1,2}\s+\w+\s+\d{4}\s*\(UTC\)/g, '') // Remove timestamps
                        .replace(/\(\s*talk\s*\)/gi, '')                                    // Remove (talk)
                        .replace(/\[\s*reply\s*\]/gi, '')                                   // Remove [reply]
                        .trim();
                    
                    console.log('Raw comment content:', JSON.stringify(commentContent));
                    
                    // Extract username from comment ID (format: c-Username-timestamp-...)
                    let username = null;
                    const idMatch = commentId.match(/[ch]-([^-]+)-/);
                    if (idMatch) {
                        username = decodeURIComponent(idMatch[1].replace(/_/g, ' '));
                    }
                    
                    // If we couldn't get username from ID, look for signature links
                    if (!username) {
                        const parentElement = commentStart.parentElement;
                        if (parentElement) {
                            const userLinks = parentElement.querySelectorAll('a[href*="/wiki/User:"]');
                            if (userLinks.length > 0) {
                                username = extractUsername(userLinks[userLinks.length - 1].getAttribute('href'));
                            }
                        }
                    }
                    
                    if (username && commentContent.trim()) {
                        if (!userStats[username]) {
                            userStats[username] = { words: 0, contributions: 0 };
                        }
                        userStats[username].words += countWords(commentContent);
                        userStats[username].contributions++;
                    }
                }
            });
        } else {
            // Fallback: traditional method for non-DiscussionTools pages
            const userLinks = sectionElement.querySelectorAll('a[href*="/wiki/User:"]');
            const validUserLinks = [];
            
            // Filter to only direct user page links (not subpages)
            userLinks.forEach(link => {
                const href = link.getAttribute('href');
                if (href.match(/\/wiki\/User:[^\/]+$/) && !href.includes('/')) {
                    const username = extractUsername(href);
                    if (username) {
                        validUserLinks.push({ element: link, username: username });
                    }
                }
            });
            
            if (validUserLinks.length === 0) {
                return { participants: 0, stats: [] };
            }
            
            // Get all text and attribute to users based on proximity to signatures
            const textWalker = document.createTreeWalker(
                sectionElement,
                NodeFilter.SHOW_TEXT,
                null,
                false
            );
            
            let textNode;
            while (textNode = textWalker.nextNode()) {
                const text = textNode.textContent.trim();
                if (text && text.length > 0) {
                    // Find nearest user signature
                    let currentElement = textNode.parentElement;
                    let foundUser = null;
                    
                    while (currentElement && currentElement !== sectionElement) {
                        const nearbyUserLinks = currentElement.querySelectorAll('a[href*="/wiki/User:"]');
                        if (nearbyUserLinks.length > 0) {
                            const username = extractUsername(nearbyUserLinks[nearbyUserLinks.length - 1].getAttribute('href'));
                            if (username) {
                                foundUser = username;
                                break;
                            }
                        }
                        currentElement = currentElement.previousElementSibling || currentElement.parentElement;
                    }
                    
                    if (foundUser) {
                        if (!userStats[foundUser]) {
                            userStats[foundUser] = { words: 0, contributions: 0 };
                        }
                        userStats[foundUser].words += countWords(text);
                        userStats[foundUser].contributions++;
                    }
                }
            }
        }
        
        // Convert to sorted array
        const statsArray = Object.entries(userStats)
            .map(([username, stats]) => ({
                username: username,
                words: stats.words,
                contributions: stats.contributions
            }))
            .sort((a, b) => b.words - a.words);
        
        return {
            participants: statsArray.length,
            stats: statsArray
        };
    }
    
    // Function to create and show popup with OOUI
    function showParticipationPopup(sectionTitle, analysisResult) {
        mw.loader.using(['oojs-ui-core', 'oojs-ui-windows', 'oojs-ui-widgets']).then(function() {
            const { participants, stats } = analysisResult;
            
            let content = `<div style="font-family: sans-serif; max-height: 400px; overflow-y: auto; padding: 10px;">`;
            content += `<h3 style="margin-top: 0; margin-bottom: 15px; font-size: 16px; line-height: 1.2;">Section: ${sectionTitle}</h3>`;
            content += `<p style="margin-bottom: 15px;"><strong>Participants: ${participants}</strong></p>`;
            
            if (stats.length > 0) {
                content += `<table style="width: 100%; border-collapse: collapse; margin-top: 10px; table-layout: fixed;">`;
                content += `<tr style="background-color: #f0f0f0; border-bottom: 1px solid #ccc;">
                              <th style="padding: 8px; text-align: left; width: 40%;">User</th>
                              <th style="padding: 8px; text-align: right; width: 30%;">Words</th>
                              <th style="padding: 8px; text-align: right; width: 30%;">Contributions</th>
                            </tr>`;
                
                stats.forEach((stat, index) => {
                    const rowStyle = index % 2 === 0 ? 'background-color: #f9f9f9;' : '';
                    content += `<tr style="${rowStyle} border-bottom: 1px solid #eee;">
                                  <td style="padding: 8px; font-weight: bold; word-wrap: break-word;">${mw.html.escape(stat.username)}</td>
                                  <td style="padding: 8px; text-align: right;">${stat.words.toLocaleString()}</td>
                                  <td style="padding: 8px; text-align: right;">${stat.contributions}</td>
                                </tr>`;
                });
                
                content += `</table>`;
            } else {
                content += `<p><em>No user contributions detected in this section.</em></p>`;
            }
            
            content += `</div>`;
            
            // Create custom dialog class
            function ParticipationDialog(config) {
                ParticipationDialog.parent.call(this, config);
            }
            OO.inheritClass(ParticipationDialog, OO.ui.MessageDialog);
            
            ParticipationDialog.static.name = 'participationDialog';
            ParticipationDialog.static.title = 'Talk Page Participation Analysis';
            ParticipationDialog.static.actions = [
                { action: 'close', label: 'Close', flags: ['primary'] }
            ];
            
            ParticipationDialog.prototype.getBodyHeight = function() {
                return 450;
            };
            
            ParticipationDialog.prototype.initialize = function() {
                ParticipationDialog.parent.prototype.initialize.apply(this, arguments);
                this.content = new OO.ui.PanelLayout({ padded: true, expanded: false });
                this.content.$element.html(content);
                this.$body.append(this.content.$element);
            };
            
            const windowManager = new OO.ui.WindowManager();
            $('body').append(windowManager.$element);
            
            const dialog = new ParticipationDialog();
            windowManager.addWindows([dialog]);
            
            windowManager.openWindow(dialog);
        });
    }
    
    // Main function to add links to section headers
    function addSectionLinks() {
        // Handle both standard H2 tags and DiscussionTools sections
        const sectionHeaders = document.querySelectorAll('h2, .mw-heading h2');
        
        sectionHeaders.forEach((header, index) => {
            // Extract section title from various possible structures
            let sectionTitle;
            const headlineSpan = header.querySelector('.mw-headline');
            if (headlineSpan) {
                // Standard structure with .mw-headline
                sectionTitle = headlineSpan.textContent.trim();
            } else {
                // DiscussionTools structure - look for comment-end span which contains the title
                const commentEnd = header.querySelector('[data-mw-comment-end]');
                if (commentEnd) {
                    sectionTitle = commentEnd.textContent.trim();
                } else {
                    // Fallback: clean up the full header text
                    sectionTitle = header.textContent
                        .replace('[analyze]', '')
                        .replace(/\[edit\]/g, '')
                        .replace(/\[subscribe\]/g, '')
                        .replace(/📌/g, '')
                        .replace(/🔗/g, '')
                        .trim();
                }
            }
            
            if (!sectionTitle) {
                sectionTitle = `Section ${index + 1}`;
            }
            
            // Skip if already has analysis link
            if (header.querySelector('a[title*="Analyze participation"]')) {
                return;
            }
            
            // Create analysis link
            const analysisLink = document.createElement('a');
            analysisLink.href = '#';
            analysisLink.textContent = '[analyze]';
            analysisLink.style.marginLeft = '10px';
            analysisLink.style.fontSize = '0.8em';
            analysisLink.style.color = '#0645ad';
            analysisLink.title = 'Analyze participation in this section';
            
            analysisLink.addEventListener('click', function(e) {
                e.preventDefault();
                
                // Find the section content - handle both standard and DiscussionTools structure
                let sectionContent = document.createElement('div');
                
                // Check if this is a DiscussionTools section (parent has mw-heading class)
                const headingContainer = header.closest('.mw-heading');
                if (headingContainer) {
                    // DiscussionTools: find content after the entire heading container
                    let currentElement = headingContainer.nextElementSibling;
                    
                    while (currentElement && !currentElement.querySelector('h2') && 
                           !currentElement.classList.contains('mw-heading')) {
                        sectionContent.appendChild(currentElement.cloneNode(true));
                        currentElement = currentElement.nextElementSibling;
                    }
                } else {
                    // Standard structure: find content after the h2
                    let currentElement = header.nextElementSibling;
                    
                    while (currentElement && currentElement.tagName !== 'H2') {
                        sectionContent.appendChild(currentElement.cloneNode(true));
                        currentElement = currentElement.nextElementSibling;
                    }
                }
                
                const analysisResult = analyzeSection(sectionContent);
                showParticipationPopup(sectionTitle, analysisResult);
            });
            
            // Add the link to the header
            header.appendChild(analysisLink);
        });
    }
    
    // Initialize when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', addSectionLinks);
    } else {
        addSectionLinks();
    }
    
})();
// </nowiki>