User:L235/wordCountsByEditor.js
Appearance
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. |
![]() | Documentation for this user script can be added at User:L235/wordCountsByEditor. |
/**
* Word Count by Editor
*
* This script provides a word counting tool for MediaWiki discussion pages.
* It analyzes comments from different editors and provides word count statistics
* organized by editor name.
*
* Key Features:
* - Counts words in discussion comments by editor
* - Allows filtering by specific page sections
* - Option to include or exclude subsections
* - Dynamic loading of Convenient Discussions if not already present
*
* Usage: Adds a "Word counts by editor" link to the page actions menu
* that opens a dialog for analyzing comment word counts.
*
* Note: This script depends on Convenient Discussions (CD). If CD is not present,
* the script will dynamically load it. This will change the formatting of the
* remainder of the talk page, but will not persist beyond a page refresh.
*/
(function () {
"use strict";
// Configuration for loading Convenient Discussions script
const CD_SCRIPT_URL =
"https://commons.wikimedia.org/w/index.php?title=User:Jack_who_built_the_house/convenientDiscussions.js&action=raw&ctype=text/javascript";
/**
* Counts words in a text string, excluding URLs and empty strings
* @param {string} text - The text to count words in
* @returns {number} Number of words (containing at least one alphanumeric character)
*/
const countWords = (text) =>
text
.replace(/https?:\/\/\S+/g, "") // Remove URLs
.split(/\s+/) // Split on whitespace
.filter((word) => word && /[A-Za-z0-9]/.test(word)).length; // Filter non-empty words with alphanumeric chars
/**
* Aggregates word counts and comment counts by editor from a collection of comments
* @param {Array} comments - Array of comment objects from Convenient Discussions
* @returns {Object} Object with editor names as keys and objects containing wordCount and commentCount as values
*/
const aggregate = (comments) => {
const totals = Object.create(null); // Create object without prototype
for (const comment of comments) {
const editorName = comment.author?.name || "Unknown";
const wordCount = countWords(comment.getText(true));
// Initialize editor entry if it doesn't exist
if (!totals[editorName]) {
totals[editorName] = { wordCount: 0, commentCount: 0 };
}
// Always increment comment count (even for comments with no words)
totals[editorName].commentCount++;
// Add word count if the comment has words
if (wordCount) {
totals[editorName].wordCount += wordCount;
}
}
return totals;
};
/**
* Gets all sections from Convenient Discussions
* @returns {Array} Array of section objects
*/
const cdSections = () => window.convenientDiscussions.sections;
/**
* Finds the shallowest (highest level) heading level among all sections
* @returns {number} The minimum heading level (1-6)
*/
const shallowestLevel = () =>
Math.min(...cdSections().map((section) => section.level ?? 6));
/**
* Gets top-level sections (sections at the shallowest heading level)
* @returns {Array} Array of top-level section objects
*/
const getTopLevelSections = () =>
cdSections().filter(
(section) => (section.level ?? 6) === shallowestLevel()
);
/**
* Ensures Convenient Discussions is loaded and ready for use
* Dynamically loads the CD script if not already present
* @returns {Promise} Promise that resolves when CD is ready
*/
function ensureCDReady() {
// If CD is already loaded and comments are available, return immediately
if (window.convenientDiscussions?.comments) {
return Promise.resolve();
}
// Reuse existing promise if already loading to prevent multiple loads
if (ensureCDReady._promise) return ensureCDReady._promise;
// Show loading notification
mw.notify("Loading Convenient Discussions…", { type: "info" });
ensureCDReady._promise = new Promise((resolve, reject) => {
// Load the CD script
mw.loader.load(CD_SCRIPT_URL);
// Wait for CD to finish parsing the page
mw.hook("convenientDiscussions.commentsReady").add(() => {
mw.notify("Convenient Discussions loaded.", { type: "info" });
resolve();
});
// Fallback timeout in case the hook never fires (network error, etc.)
setTimeout(() => {
if (!window.convenientDiscussions?.comments) {
reject(new Error("Convenient Discussions failed to load"));
} else {
resolve();
}
}, 30000); // 30 second timeout
});
return ensureCDReady._promise;
}
// Dialog instance - created once and reused
let dialog;
/**
* Opens the word count dialog with filtering options
*/
function openDialog() {
const cd = window.convenientDiscussions;
if (!cd?.comments) {
// Safety check - should not occur if ensureCDReady worked
return mw.notify(
"Word-count script: Convenient Discussions not ready.",
{ type: "error" }
);
}
const sections = getTopLevelSections();
// Create dropdown for section selection
const dropdownOptions = [
new OO.ui.OptionWidget({ data: null, label: "Whole page" }),
];
sections.forEach((section, index) => {
const label =
typeof section.getHeadingText === "function"
? section.getHeadingText()
: section.headingElement
? $(section.headingElement).text().trim()
: "(untitled)";
dropdownOptions.push(
new OO.ui.OptionWidget({ data: index, label })
);
});
const dropdown = new OO.ui.DropdownWidget({
menu: { items: dropdownOptions },
});
// Create checkbox for including subsections
const checkbox = new OO.ui.CheckboxInputWidget({ selected: true });
const checkboxField = new OO.ui.FieldLayout(checkbox, {
label: "Include subsections",
align: "inline",
});
// Show/hide subsection checkbox based on whether a specific section is selected
dropdown
.getMenu()
.on("choose", (option) =>
checkboxField.toggle(option.getData() !== null)
);
// Create results display area
const output = new OO.ui.MultilineTextInputWidget({
readOnly: true,
rows: 12,
});
/**
* Performs the word counting operation based on current selections
*/
function runCount() {
const selectedChoice = dropdown
.getMenu()
.findSelectedItem()
.getData();
let commentPool = cd.comments;
// If a specific section is selected, filter comments accordingly
if (selectedChoice !== null) {
const rootSection = sections[selectedChoice];
const includeSubsections = checkbox.isSelected();
commentPool = cd.comments.filter((comment) => {
const commentSection = comment.section;
if (!commentSection) return false;
// Always include comments from the exact selected section
if (commentSection === rootSection) return true;
if (!includeSubsections) return false;
// Use heading level analysis to determine if comment is in a subsection
const allSections = cdSections();
const rootIndex = allSections.indexOf(rootSection);
const commentIndex = allSections.indexOf(commentSection);
if (commentIndex < rootIndex) return false; // Comment section comes before root
const rootLevel = rootSection.level ?? 0;
// Walk backwards from comment section to find nearest higher/equal heading
for (let i = commentIndex - 1; i >= 0; i--) {
const candidateSection = allSections[i];
if ((candidateSection.level ?? 0) <= rootLevel) {
return candidateSection === rootSection; // Found the root section
}
}
return false;
});
}
// Aggregate and display results
const editorStats = aggregate(commentPool);
output.setValue(
Object.keys(editorStats)
.sort(
(a, b) =>
editorStats[b].wordCount - editorStats[a].wordCount
) // Sort by word count descending
.map(
(editor) =>
`${editor}: ${editorStats[
editor
].wordCount.toLocaleString()} words, ${
editorStats[editor].commentCount
} comment${
editorStats[editor].commentCount !== 1
? "s"
: ""
}`
)
.join("\n") || "No comments detected."
);
}
// Create dialog if it doesn't exist yet
if (!dialog) {
// Define custom dialog class
function WordCountDialog(config) {
WordCountDialog.super.call(this, config);
}
OO.inheritClass(WordCountDialog, OO.ui.ProcessDialog);
// Dialog configuration
WordCountDialog.static.name = "wcDialog";
WordCountDialog.static.title = "Word counts";
WordCountDialog.static.actions = [
{
action: "count",
label: "Count words",
flags: ["progressive"],
},
{ action: "close", label: "Close", flags: ["safe"] },
];
// Initialize dialog content
WordCountDialog.prototype.initialize = function () {
WordCountDialog.super.prototype.initialize.apply(
this,
arguments
);
const panel = new OO.ui.PanelLayout({ padded: true });
panel.$element.append(
new OO.ui.FieldsetLayout({
items: [
new OO.ui.FieldLayout(dropdown, {
label: "Section",
align: "top",
}),
checkboxField,
new OO.ui.FieldLayout(output, {
label: "Results",
align: "top",
}),
],
}).$element
);
this.$body.append(panel.$element);
};
// Handle dialog actions
WordCountDialog.prototype.getActionProcess = function (action) {
if (action === "count") return new OO.ui.Process(runCount);
if (action === "close")
return new OO.ui.Process(() => this.close());
return WordCountDialog.super.prototype.getActionProcess.call(
this,
action
);
};
// Create and set up dialog
dialog = new WordCountDialog();
const windowManager = new OO.ui.WindowManager();
$(document.body).append(windowManager.$element);
windowManager.addWindows([dialog]);
dialog.windowManager = windowManager;
}
// Open the dialog
dialog.windowManager.openWindow(dialog);
}
// Add portlet link to page actions menu
mw.loader
.using(["oojs-ui-core", "oojs-ui-widgets", "oojs-ui-windows"])
.then(() => {
mw.util
.addPortletLink(
"p-cactions", // Page actions portlet
"#",
"Word counts by editor",
"ca-wordcounts-by-editor",
"Open word-count dialog"
)
.addEventListener("click", (event) => {
event.preventDefault();
// Ensure CD is ready, then open dialog
ensureCDReady()
.then(openDialog)
.catch((error) => {
console.error(error);
mw.notify(
"Word-count script: failed to load Convenient Discussions.",
{ type: "error" }
);
});
});
});
})();