User:Polygnotus/Scripts/WordCount.js
Appearance
From Wikipedia, the free encyclopedia
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump.
This code will be executed when previewing this page.
This code will be executed when previewing this page.
Documentation for this user script can be added at User:Polygnotus/Scripts/WordCount.
// 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) functioncountWords(text){ if(!text)return0; // Create a temporary element to parse HTML and extract text without signature elements consttempDiv=document.createElement('div'); tempDiv.innerHTML=text; // Remove all signature-related elements constelementsToRemove=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 letplainText=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)); constwords=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 returnwords.length; } // Function to extract username from user page link functionextractUsername(href){ constuserMatch=href.match(/\/wiki\/User:([^\/]+)$/); if(userMatch){ returndecodeURIComponent(userMatch[1].replace(/_/g,' ')); } returnnull; } // Function to analyze participation in a section functionanalyzeSection(sectionElement){ constuserStats={}; // Look for DiscussionTools comment markers constcommentElements=sectionElement.querySelectorAll('[data-mw-comment-start]'); if(commentElements.length>0){ // DiscussionTools format - analyze each comment commentElements.forEach(commentStart=>{ constcommentId=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 letcommentEnd=null; constallElements=sectionElement.querySelectorAll('*'); for(letelofallElements){ if(el.getAttribute('data-mw-comment-end')===commentId){ commentEnd=el; break; } } if(commentEnd){ // Extract comment content between start and end, excluding signature elements letcommentContent=''; letcurrentNode=commentStart.nextSibling; while(currentNode&¤tNode!==commentEnd){ if(currentNode.nodeType===Node.TEXT_NODE){ commentContent+=currentNode.textContent; }elseif(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-...) letusername=null; constidMatch=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){ constparentElement=commentStart.parentElement; if(parentElement){ constuserLinks=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 constuserLinks=sectionElement.querySelectorAll('a[href*="/wiki/User:"]'); constvalidUserLinks=[]; // Filter to only direct user page links (not subpages) userLinks.forEach(link=>{ consthref=link.getAttribute('href'); if(href.match(/\/wiki\/User:[^\/]+$/)&&!href.includes('/')){ constusername=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 consttextWalker=document.createTreeWalker( sectionElement, NodeFilter.SHOW_TEXT, null, false ); lettextNode; while(textNode=textWalker.nextNode()){ consttext=textNode.textContent.trim(); if(text&&text.length>0){ // Find nearest user signature letcurrentElement=textNode.parentElement; letfoundUser=null; while(currentElement&¤tElement!==sectionElement){ constnearbyUserLinks=currentElement.querySelectorAll('a[href*="/wiki/User:"]'); if(nearbyUserLinks.length>0){ constusername=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 conststatsArray=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 functionshowParticipationPopup(sectionTitle,analysisResult){ mw.loader.using(['oojs-ui-core','oojs-ui-windows','oojs-ui-widgets']).then(function(){ const{participants,stats}=analysisResult; letcontent=`<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)=>{ constrowStyle=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 functionParticipationDialog(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(){ return450; }; ParticipationDialog.prototype.initialize=function(){ ParticipationDialog.parent.prototype.initialize.apply(this,arguments); this.content=newOO.ui.PanelLayout({padded:true,expanded:false}); this.content.$element.html(content); this.$body.append(this.content.$element); }; constwindowManager=newOO.ui.WindowManager(); $('body').append(windowManager.$element); constdialog=newParticipationDialog(); windowManager.addWindows([dialog]); windowManager.openWindow(dialog); }); } // Main function to add links to section headers functionaddSectionLinks(){ // Handle both standard H2 tags and DiscussionTools sections constsectionHeaders=document.querySelectorAll('h2, .mw-heading h2'); sectionHeaders.forEach((header,index)=>{ // Extract section title from various possible structures letsectionTitle; constheadlineSpan=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 constcommentEnd=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 constanalysisLink=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 letsectionContent=document.createElement('div'); // Check if this is a DiscussionTools section (parent has mw-heading class) constheadingContainer=header.closest('.mw-heading'); if(headingContainer){ // DiscussionTools: find content after the entire heading container letcurrentElement=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 letcurrentElement=header.nextElementSibling; while(currentElement&¤tElement.tagName!=='H2'){ sectionContent.appendChild(currentElement.cloneNode(true)); currentElement=currentElement.nextElementSibling; } } constanalysisResult=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(); } })();