// Improved Wikipedia Duplicate Parameters Detector
// Add this to your common.js file
// This script combines the best features of both duplicate parameter detection scripts
jQuery(document).ready(function($) {
// Only run on edit pages
if (mw.config.get('wgAction') !== 'edit' && mw.config.get('wgNamespaceNumber') !== -1) {
return;
}
// Configuration options (can be overridden in user scripts)
var config = {
buttonText: 'Check Duplicate Parameters',
summaryText: "Clean up [[Category:Pages using duplicate arguments in template calls|duplicate template arguments]]",
moreFoundMessage: "More duplicates found, fix some and run again!",
noneFoundMessage: 'No duplicate parameters found.',
showResultsBox: true,
showAlertBox: false, // Alerts disabled by default
maxAlertsBeforeMessage: 5,
debugMode: false // Add debug mode to help diagnose issues
};
// Allow overriding configuration
if (typeof findargdupseditsummary === 'string') { config.summaryText = findargdupseditsummary; }
if (typeof findargdupsmorefound === 'string') { config.moreFoundMessage = findargdupsmorefound; }
if (typeof findargdupslinktext === 'string') { config.buttonText = findargdupslinktext; }
if (typeof findargdupsnonefound === 'string') { config.noneFoundMessage = findargdupsnonefound; }
if (typeof findargdupsresultsbox === 'string') { config.showResultsBox = true; }
var myContent = document.getElementsByName('wpTextbox1')[0] || $('#wpTextbox1')[0];
// Add both UI options (button next to title and toolbar link)
// 1. Add button next to title
$('#firstHeadingTitle').after(
$('<button>')
.attr('id', 'check-duplicate-params')
.text(config.buttonText)
.css({
'margin-left': '15px',
'font-size': '0.8em',
'padding': '3px 8px',
'cursor': 'pointer'
})
.click(function(e) {
e.preventDefault();
findDuplicateParameters();
})
);
// 2. Add toolbar link
mw.loader.using(['mediawiki.util']).done(function() {
var portletlink = mw.util.addPortletLink('p-tb', '#', config.buttonText, 't-fdup');
$(portletlink).click(function(e) {
e.preventDefault();
findDuplicateParameters();
});
});
// Add a message area to display results
if (!$('#duplicate-params-message').length) {
$('body').append(
$('<div>')
.attr('id', 'duplicate-params-message')
.css({
'position': 'fixed',
'bottom': '20px',
'right': '20px',
'padding': '10px',
'background-color': '#f8f9fa',
'border': '1px solid #a2a9b1',
'border-radius': '3px',
'box-shadow': '0 2px 5px rgba(0, 0, 0, 0.1)',
'z-index': '1000',
'display': 'none'
})
);
}
// Add a debug log area if debug mode is enabled
if (config.debugMode && !$('#debug-log').length) {
$('body').append(
$('<div>')
.attr('id', 'debug-log')
.css({
'position': 'fixed',
'top': '20px',
'right': '20px',
'width': '600px',
'max-height': '400px',
'overflow-y': 'auto',
'padding': '10px',
'background-color': '#f8f9fa',
'border': '1px solid #a2a9b1',
'border-radius': '3px',
'box-shadow': '0 2px 5px rgba(0, 0, 0, 0.1)',
'z-index': '1000',
'font-size': '0.8em'
})
);
}
// Debug log function
function debugLog(message) {
if (config.debugMode && $('#debug-log').length) {
$('#debug-log').append($('<div>').text(message));
$('#debug-log').scrollTop($('#debug-log')[0].scrollHeight);
}
}
// Function to add results box above the edit summary
function addResultsBox(text, num) {
var div = document.getElementById('wpSummaryLabel')?.parentNode;
if (div) {
if (num < 2) {
if (document.getElementById('FindArgDupsResultsBox')) {
document.getElementById('FindArgDupsResultsBox').innerHTML = '';
} else {
div.innerHTML = '<div id="FindArgDupsResultsBox"></div>' + div.innerHTML;
}
}
let div1 = document.getElementById('FindArgDupsResultsBox');
if (div1) {
text = text.replace(/</g, '<').replace(/>/g, '>');
div1.innerHTML = div1.innerHTML + '<div class="FindArgDupsResultsBox" ' +
'id="FindArgDupsResultsBox-' + num + '" ' +
'style="max-height:5em; overflow:auto; padding:5px; border:#aaa 1px solid; ' +
'background-color:cornsilk;">' + text + '</div>' + "\n";
}
}
}
// Function to clear the results box
function clearResultsBox() {
var div = document.getElementById('wpSummaryLabel')?.parentNode;
if (div) {
if (document.getElementById('FindArgDupsResultsBox')) {
document.getElementById('FindArgDupsResultsBox').innerHTML = '';
}
}
}
// Function to show a message
function showMessage(message, isSuccess) {
if (config.showResultsBox) {
clearResultsBox();
addResultsBox(message, 1);
}
const $message = $('#duplicate-params-message');
$message.text(message)
.css('background-color', isSuccess ? '#d5fdf4' : '#fdd')
.fadeIn()
.delay(5000)
.fadeOut();
}
// Create and show a popup for the user to choose which parameter to keep
function showParameterChoicePopup(paramName, duplicates, templateText, callback) {
debugLog("Showing parameter choice popup for \"" + paramName + "\" with " + duplicates.length + " values");
// Create a dialog if it doesn't exist
if (!$('#parameter-choice-dialog').length) {
$('body').append('<div id="parameter-choice-dialog" title="Duplicate Parameters Found"></div>');
}
// Clear previous content
const $dialog = $('#parameter-choice-dialog');
$dialog.empty();
// Add explanation text
$dialog.append('<p>Duplicate parameter "' + paramName + '" found with different values. Please select which one to keep:</p>');
// Show the template where the duplicates were found
$dialog.append('<p style="max-height: 100px; overflow: auto; border: 1px solid #ddd; padding: 5px; background-color: #f8f8f8;">' +
templateText.replace(/</g, '<').replace(/>/g, '>') + '</p>');
// Create the options list
const $list = $('<div class="parameter-options"></div>');
// Log the duplicate values for debugging
for (let i = 0; i < duplicates.length; i++) {
debugLog("Option " + i + ": " + paramName + "=" + duplicates[i].value);
}
duplicates.forEach(function(param, index) {
const $option = $('<div class="parameter-option"></div>')
.append($('<input type="radio">')
.attr('name', 'param-choice')
.attr('id', 'param-' + index)
.attr('value', index)
.prop('checked', index === 0)
)
.append($('<label></label>')
.attr('for', 'param-' + index)
.text(paramName + ' = ' + param.value)
);
$list.append($option);
});
$dialog.append($list);
// Open the dialog
try {
$dialog.dialog({
modal: true,
width: 400,
buttons: {
"Keep Selected": function() {
const selectedIndex = parseInt($('input[name="param-choice"]:checked').val(), 10);
debugLog("User selected to keep option " + selectedIndex + ": " + paramName + "=" + duplicates[selectedIndex].value);
callback(selectedIndex);
$(this).dialog("close");
},
Cancel: function() {
debugLog("User cancelled parameter choice");
$(this).dialog("close");
}
}
});
} catch (e) {
debugLog("Error showing dialog: " + e.message);
// Fallback if dialog fails - keep the first option
callback(0);
}
}
// Manual DOM-based parameter selector for when jQuery dialog fails
function createSimpleDialog(paramName, values, callback) {
const dialog = document.createElement('div');
dialog.id = 'manual-parameter-choice';
dialog.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:white;padding:20px;border:1px solid #ccc;box-shadow:0 0 10px rgba(0,0,0,0.2);z-index:1000;';
let html = '<h3>Duplicate Parameter Found</h3>' +
'<p>Parameter ' + paramName + ' has duplicate values. Choose which one to keep:</p>';
values.forEach(function(param, index) {
html += '<div>' +
'<input type="radio" id="value-' + index + '" name="' + paramName + '" value="' + index + '"' +
(index === 0 ? ' checked' : '') + '>' +
'<label for="value-' + index + '">' + paramName + ' = ' + param.value + '</label>' +
'</div>';
});
html += '<div style="margin-top:15px;text-align:right;">' +
'<button id="cancel-btn" style="margin-right:10px;padding:5px 10px;">Cancel</button>' +
'<button id="confirm-btn" style="padding:5px 10px;">Keep Selected</button>' +
'</div>';
dialog.innerHTML = html;
document.body.appendChild(dialog);
// Add event listeners
document.getElementById('confirm-btn').addEventListener('click', function() {
const selected = document.querySelector('input[name="' + paramName + '"]:checked');
const selectedIndex = parseInt(selected.value, 10);
document.body.removeChild(dialog);
callback(selectedIndex);
});
document.getElementById('cancel-btn').addEventListener('click', function() {
document.body.removeChild(dialog);
});
}
// Main function to find duplicate parameters
function findDuplicateParameters() {
if (!myContent) return;
// Flag used to determine if we have issued an alert popup
var alertsIssued = 0;
// Flag used to determine if we've selected one of the problem templates yet
var selectedOne = false;
// Array used to hold the list of unnested templates
var templateList = [];
// Variable to store the original content
var originalContent = myContent.value;
// Variable to store the modified content when removing duplicates
var updatedContent = originalContent;
// Count of auto-removed duplicates
var autoRemovedCount = 0;
// Count of templates with duplicates
var duplicateTemplatesCount = 0;
// Count of parameters requiring user choice
var userChoiceCount = 0;
// Helper function to escape regex special characters
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Helper function to properly remove a parameter from template text
function removeParamFromTemplate(template, paramName, valueToRemove) {
// This regex matches the entire parameter including the | at the beginning
const paramRegex = new RegExp("\\|(\\s*" + escapeRegExp(paramName) + "\\s*=\\s*" + escapeRegExp(valueToRemove) + "\\s*)(?=\\||}})", "g");
return template.replace(paramRegex, '');
}
// Preprocess the text to handle various special cases
function preprocessText(text) {
// Copy the contents of the text window so we can modify it without problems
var processed = text;
// Remove some includeonly, noinclude, and onlyinclude tags
processed = processed.replace(/<\/?[ ]*(?:includeonly|noinclude|onlyinclude)[ ]*>/gi, '');
// Remove PAGENAME, BASEPAGENAME, ... nested inside of triple braces
processed = processed.replace(/\{\{\{[^\{\}]*\|[ ]*\{\{[A-Z]+\}\}\}\}\}/g, '');
// Mangle some ref tags
processed = processed.replace(/(<ref[^<>=]*name[ ]*)=/gi, '$1=');
processed = processed.replace(/(<ref[^<>=]*group[ ]*)=/gi, '$1=');
// Mangle some math tags
let loopcount = 0;
while ((processed.search(/<[\s]*math[^<>]*>[^<>=]*=/gi) >= 0) && (loopcount < 10)) {
processed = processed.replace(/(<[\s]*math[^<>]*>[^<>=]*)=/gi, '$1=');
loopcount++;
}
// Remove some triple braces and parserfunctions inside of triple braces
loopcount = 0;
while ((processed.search(/\{\{\{[^\{\}]*\}\}\}/g) >= 0) && (loopcount < 5)) {
processed = processed.replace(/\{\{\{[^\{\}]*\}\}\}/g, '');
processed = processed.replace(/\{\{#[a-z]+:[^{}=]*\}\}/gi, '');
loopcount++;
}
// Replace some bare braces with HTML equivalent
processed = processed.replace(/([^\{])\{([^\{])/g, '$1{$2');
processed = processed.replace(/([^\}])\}([^\}])/g, '$1}$2');
// Remove newlines and tabs which confuse the regexp search
processed = processed.replace(/[\s]/gm, ' ');
// Compress whitespace
processed = processed.replace(/[\s][\s]+/gm, ' ');
// Remove some nowiki and pre text
processed = processed.replace(/<nowiki[^<>]*>(?:<[^\/]|[^<])*<\/nowiki[^<>]*>/gi, '');
processed = processed.replace(/<pre[^<>]*>(?:<[^\/]|[^<])*<\/pre[^<>]*>/gi, '');
// Remove some HTML comments
processed = processed.replace(/<!--(?:[^>]|[^\-]>|[^\-]->)*-->/gm, '');
// Modify some = inside of file/image/wikilinks which cause false positives
loopcount = 0;
while ((processed.search(/\[\[[^\[\]\{\}]*=/gi) >= 0) && (loopcount < 5)) {
processed = processed.replace(/(\[\[[^\[\]\{\}]*)=/gi, '$1=');
loopcount++;
}
return processed;
}
// Function to unnest templates and extract them to the templateList array
function extractTemplates(text) {
var processed = text;
var loopcount = 0;
while ((processed.search(/(?:\{\{|\}\})/g) >= 0) && (loopcount < 20)) {
// Replace some bare braces with HTML equivalent
processed = processed.replace(/([^\{])\{([^\{])/g, '$1{$2');
processed = processed.replace(/([^\}])\}([^\}])/g, '$1}$2');
// Split into chunks, isolating the unnested templates
var strlist = processed.split(/(\{\{[^\{\}]*\}\})/);
// Loop through the chunks, removing the unnested templates
for (let i = 0; i < strlist.length; i++) {
if (strlist[i].search(/^\{\{[^\{\}]*\}\}$/) >= 0) {
templateList.push(strlist[i]);
strlist[i] = '';
}
}
// Join the chunks back together for the next iteration
processed = strlist.join('');
loopcount++;
}
}
// Function to add numbers for unnamed parameters
function processUnnamedParameters(template) {
let processed = template;
// Add numbers for unnamed parameters in #invoke templates
processed = processed.replace(/(\{\{[\s_]*#invoke[\s ]*:[^{}\|]*)\|([^{}\|=]*\|)/gi, '$1|0=$2');
// Add numbers for other unnamed parameters
let unp = 0;
while ((processed.search(/(\{\{(?:[^{}\[\]]|\[\[[^\[\]]*\]\])*?\|)((?:[^{}\[\]=\|]|\[[^\[\]=]*\]|\[\[[^\[\]]*\]\])*(?:\||\}\}))/) >= 0) && (unp < 25)) {
unp++;
processed = processed.replace(/(\{\{(?:[^{}\[\]]|\[\[[^\[\]]*\]\])*?\|)((?:[^{}\[\]=\|]|\[[^\[\]=]*\]|\[\[[^\[\]]*\]\])*(?:\||\}\}))/, '$1' + unp + '=$2');
}
return processed;
}
// Function to extract a parameter value from a template
function extractParameterValue(template, paramStart, nextParam) {
let valueEnd = 0;
let nestedCount = 0;
let inLink = false;
for (let j = 0; j < nextParam.length; j++) {
if (nextParam[j] === '[' && nextParam[j+1] === '[') {
inLink = true;
} else if (nextParam[j] === ']' && nextParam[j+1] === ']') {
inLink = false;
} else if (nextParam[j] === '{') {
nestedCount++;
} else if (nextParam[j] === '}') {
nestedCount--;
} else if ((nextParam[j] === '|' || (nextParam[j] === '}' && nextParam[j+1] === '}')) && nestedCount <= 0 && !inLink) {
valueEnd = j;
break;
}
}
if (valueEnd === 0) {
valueEnd = nextParam.indexOf('|');
if (valueEnd === -1) {
valueEnd = nextParam.indexOf('}}');
if (valueEnd === -1) {
valueEnd = nextParam.length;
}
}
}
return nextParam.substring(0, valueEnd).trim();
}
// Function to find parameter duplicates in a template
function findParameterDuplicates(template) {
// Process ref tags inside templates
let processedTemplate = template;
let j = 0;
while ((processedTemplate.search(/<ref[^<>\/]*>(?:<[^\/]|[^<])*=/gi) >= 0) && (j < 50)) {
processedTemplate = processedTemplate.replace(/(<ref[^<>\/]*>(?:<[^\/]|[^<])*)=/gi, '$1=');
j++;
}
// Add numbers for unnamed parameters
processedTemplate = processUnnamedParameters(processedTemplate);
// Regular expression which matches a template arg
const argexp = /\|\s*([^|={}\[\]]+)\s*=\s*([^|{}]+)(?=\||}})/g;
// Map to store parameter names and their values
const paramMap = new Map();
// Find all parameters in the template
let match;
while ((match = argexp.exec(processedTemplate)) !== null) {
const paramName = match[1].trim();
const paramValue = match[2].trim();
const paramFull = match[0];
const paramPosition = match.index;
debugLog("Found parameter: " + paramName + "=" + paramValue);
// Check if this parameter already exists
if (paramMap.has(paramName)) {
const existingValues = paramMap.get(paramName);
existingValues.push({
value: paramValue,
original: paramFull,
position: paramPosition
});
paramMap.set(paramName, existingValues);
} else {
paramMap.set(paramName, [{
value: paramValue,
original: paramFull,
position: paramPosition
}]);
}
}
// Check for duplicates
const duplicateParams = [];
for (const [paramName, values] of paramMap.entries()) {
if (values.length > 1) {
// Check if values are all the same
const firstValue = values[0].value;
const allSameValue = values.every(param => param.value === firstValue);
debugLog("Duplicate parameter \"" + paramName + "\" found with " + values.length + " occurrences");
debugLog("All values same? " + allSameValue);
duplicateParams.push({
name: paramName,
values: values,
allSameValue: allSameValue
});
}
}
return duplicateParams;
}
// Function to fix duplicate parameters in the content
function fixDuplicateParameters(template, duplicateParams) {
// Process each duplicate parameter
for (const duplicate of duplicateParams) {
const paramName = duplicate.name;
const values = duplicate.values;
const allSameValue = duplicate.allSameValue;
debugLog("Processing duplicate: " + paramName + ", all same value: " + allSameValue);
if (allSameValue) {
// If all duplicates have the same value, keep only the first one
for (let i = 1; i < values.length; i++) {
const duplicateParam = values[i].original;
const startPos = template.indexOf(duplicateParam);
if (startPos !== -1) {
// Get the text after the parameter name and =
const afterEqualSign = template.substring(startPos + duplicateParam.length);
// Find the end of the parameter value
const paramValue = extractParameterValue(template, startPos + duplicateParam.length, afterEqualSign);
// Use the helper function to properly remove the parameter
updatedContent = removeParamFromTemplate(updatedContent, paramName, paramValue);
autoRemovedCount++;
debugLog("Auto-removed duplicate: " + paramName + "=" + paramValue);
}
}
} else {
// If duplicates have different values, show a popup for user to choose
userChoiceCount++;
debugLog("Showing choice dialog for " + paramName + " with " + values.length + " different values");
try {
showParameterChoicePopup(paramName, values, template, function(selectedIndex) {
// Keep the selected parameter and remove others
debugLog("User chose to keep " + paramName + "=" + values[selectedIndex].value);
// Find positions of all duplicates in the original content
const chosenValue = values[selectedIndex].value;
// Create a temporary copy of the original content
let tempContent = originalContent;
// Process each duplicate to remove all except the selected one
for (let i = 0; i < values.length; i++) {
// Skip the one we want to keep
if (i === selectedIndex) continue;
const valueToRemove = values[i].value;
debugLog("Removing duplicate: " + paramName + "=" + valueToRemove);
// Remove this specific parameter value from the template
tempContent = removeParamFromTemplate(tempContent, paramName, valueToRemove);
}
// Update the textbox with the modified content
myContent.value = tempContent;
updatedContent = tempContent;
showMessage("Kept parameter: " + paramName + "=" + chosenValue, true);
});
} catch (e) {
debugLog("Error showing jQuery dialog: " + e.message + ". Falling back to simple dialog.");
// Fallback to simple DOM-based dialog if jQuery dialog fails
createSimpleDialog(paramName, values, function(selectedIndex) {
// Keep the selected parameter and remove others
debugLog("User chose to keep " + paramName + "=" + values[selectedIndex].value);
// Find positions of all duplicates in the original content
const chosenValue = values[selectedIndex].value;
// Create a temporary copy of the original content
let tempContent = originalContent;
// Process each duplicate to remove all except the selected one
for (let i = 0; i < values.length; i++) {
// Skip the one we want to keep
if (i === selectedIndex) continue;
const valueToRemove = values[i].value;
debugLog("Removing duplicate: " + paramName + "=" + valueToRemove);
// Remove this specific parameter value from the template
tempContent = removeParamFromTemplate(tempContent, paramName, valueToRemove);
}
// Update the textbox with the modified content
myContent.value = tempContent;
updatedContent = tempContent;
showMessage("Kept parameter: " + paramName + "=" + chosenValue, true);
});
}
}
}
}
// Main processing starts here
debugLog("Starting duplicate parameter detection");
// Preprocess the text
const processedText = preprocessText(originalContent);
// Extract templates
extractTemplates(processedText);
debugLog("Found " + templateList.length + " templates to check");
// Process each template to find duplicates
for (let i = 0; i < templateList.length; i++) {
debugLog("Checking template " + (i+1) + " of " + templateList.length);
const duplicateParams = findParameterDuplicates(templateList[i]);
if (duplicateParams.length > 0) {
duplicateTemplatesCount++;
debugLog("Found " + duplicateParams.length + " duplicate parameters in template " + (i+1));
if (!selectedOne) {
// Try to select the template in the text area
const templatePattern = templateList[i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '\\s*');
const selectMatch = originalContent.match(new RegExp(templatePattern));
if (selectMatch !== null) {
myContent.setSelectionRange(selectMatch.index, selectMatch.index + selectMatch[0].length);
myContent.focus();
selectedOne = true;
}
}
// Display information about the duplicates
if (alertsIssued < config.maxAlertsBeforeMessage) {
const duplicateNames = duplicateParams.map(param => param.name).join('", "');
if (config.showResultsBox) {
addResultsBox("Duplicate \"" + duplicateNames + "\" in\n" + templateList[i], alertsIssued + 1);
}
alertsIssued++;
// Try to fix the duplicates
fixDuplicateParameters(templateList[i], duplicateParams);
} else if (alertsIssued === config.maxAlertsBeforeMessage) {
// Show "more found" message in results box instead of alert
if (config.showResultsBox) {
addResultsBox(config.moreFoundMessage, alertsIssued + 1);
}
alertsIssued++;
}
}
}
// If we had duplicates, update the textbox content and edit summary
if (duplicateTemplatesCount > 0) {
// Update edit summary
const editSummary = document.getElementsByName('wpSummary')[0];
if (typeof editSummary === 'object') {
if (editSummary.value.indexOf(config.summaryText) === -1) {
if (editSummary.value.match(/[^\*\/\s][^\/\s]?\s*$/)) {
editSummary.value += '; ' + config.summaryText;
} else {
editSummary.value += config.summaryText;
}
}
}
if (autoRemovedCount > 0 && userChoiceCount === 0) {
// Update the textbox if we automatically removed duplicates and there are no user choices
myContent.value = updatedContent;
showMessage("Found " + duplicateTemplatesCount + " template(s) with duplicate parameter(s). Automatically removed " + autoRemovedCount + " identical duplicates.", true);
} else if (userChoiceCount > 0) {
showMessage("Found " + duplicateTemplatesCount + " template(s) with " + userChoiceCount + " parameter(s) that need user choices. Please select which values to keep.", true);
} else if (alertsIssued === 0) {
showMessage(config.noneFoundMessage, true);
}
} else {
// No duplicates found
showMessage(config.noneFoundMessage, true);
if (config.showResultsBox) {
clearResultsBox();
}
}
}
});