User:Polygnotus/Scripts/DuplicateParameters.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:Polygnotus/Scripts/DuplicateParameters. |
// 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();
}
}
}
});