User:Polygnotus/Scripts/DuplicateParameters.js
// <nowiki>
// Enhanced Duplicate Parameter Finder for MediaWiki
// Combines robust template parsing, automatic fixes, and comprehensive error handling
// Version: 2.3 - Added nested template support
// https://en.wikipedia.org/wiki/Category:Articles_using_duplicate_arguments_in_template_calls
(function() {
'use strict';
// ============================================================================
// CONFIGURATION
// ============================================================================
const CONFIG = {
buttonText: 'Check Duplicate Parameters',
summaryText: 'Clean up [[:Category:Articles 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,
maxAlertsBeforeMessage: 10,
maxTemplatesPerRun: 1000,
debugMode: true // Always on for debugging
};
// Allow user overrides
if (typeof window.findargdupseditsummary === 'string') CONFIG.summaryText = window.findargdupseditsummary;
if (typeof window.findargdupsmorefound === 'string') CONFIG.moreFoundMessage = window.findargdupsmorefound;
if (typeof window.findargdupslinktext === 'string') CONFIG.buttonText = window.findargdupslinktext;
if (typeof window.findargdupsnonefound === 'string') CONFIG.noneFoundMessage = window.findargdupsnonefound;
if (typeof window.findargdupsresultsbox === 'string') CONFIG.showResultsBox = true;
if (typeof window.findargdupsnoalertbox === 'string') { CONFIG.showAlertBox = false; CONFIG.showResultsBox = true; }
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
function debugLog(message, data) {
if (CONFIG.debugMode) {
console.log('[DuplicateParameters]', message, data !== undefined ? data : '');
}
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// ============================================================================
// TEMPLATE PARSER
// ============================================================================
class TemplateParser {
constructor(text) {
this.text = text;
this.position = 0;
}
/**
* Extract all templates from text with proper brace matching
* Now includes nested templates
*/
static extractTemplates(text) {
console.log('=== EXTRACTING TEMPLATES (INCLUDING NESTED) ===');
const templates = [];
let pos = 0;
try {
while (pos < text.length) {
const start = text.indexOf('{{', pos);
if (start === -1) break;
const end = TemplateParser.findMatchingCloseBraces(text, start);
if (end !== -1) {
const templateText = text.substring(start, end);
console.log(`Template ${templates.length}: start=${start}, end=${end}, length=${templateText.length}`);
console.log(` First 100 chars: ${templateText.substring(0, 100)}`);
templates.push({
text: templateText,
start: start,
end: end,
level: 0 // Top-level template
});
// Now search for nested templates within this template
const nestedTemplates = TemplateParser.extractNestedTemplates(templateText, start, 1);
templates.push(...nestedTemplates);
pos = end;
} else {
console.log(` No matching close braces found for opening at ${start}`);
pos = start + 2;
}
}
} catch (error) {
console.error('Error extracting templates:', error);
}
console.log(`Total templates extracted (including nested): ${templates.length}`);
return templates;
}
/**
* Extract nested templates from within a template
* @param {string} templateText - The template text to search within
* @param {number} baseOffset - The offset of this template in the original document
* @param {number} level - The nesting level
*/
static extractNestedTemplates(templateText, baseOffset, level) {
console.log(` Searching for nested templates at level ${level}`);
const nestedTemplates = [];
// Skip the outer {{ and }}
const innerText = templateText.substring(2, templateText.length - 2);
let pos = 0;
try {
while (pos < innerText.length) {
const start = innerText.indexOf('{{', pos);
if (start === -1) break;
// Find matching close for this nested template
const end = TemplateParser.findMatchingCloseBraces(innerText, start);
if (end !== -1) {
const nestedText = innerText.substring(start, end);
const absoluteStart = baseOffset + 2 + start; // +2 for the outer {{
const absoluteEnd = absoluteStart + nestedText.length;
console.log(` Nested template at level ${level}: start=${absoluteStart}, end=${absoluteEnd}`);
nestedTemplates.push({
text: nestedText,
start: absoluteStart,
end: absoluteEnd,
level: level
});
// Recursively search for templates nested even deeper
const deeperNested = TemplateParser.extractNestedTemplates(nestedText, absoluteStart, level + 1);
nestedTemplates.push(...deeperNested);
pos = end;
} else {
pos = start + 2;
}
}
} catch (error) {
console.error(`Error extracting nested templates at level ${level}:`, error);
}
console.log(` Found ${nestedTemplates.length} nested template(s) at level ${level}`);
return nestedTemplates;
}
/**
* Find matching closing braces for opening {{
*/
static findMatchingCloseBraces(text, startPos) {
let depth = 0;
let i = startPos;
let inComment = false;
let inNowiki = false;
let inPre = false;
try {
while (i < text.length) {
// Handle HTML comments
if (!inNowiki && !inPre && i < text.length - 3 && text.substring(i, i + 4) === '<!--') {
inComment = true;
i += 4;
continue;
}
if (inComment && i < text.length - 2 && text.substring(i, i + 3) === '-->') {
inComment = false;
i += 3;
continue;
}
// Handle nowiki tags
if (!inComment && !inPre && i < text.length - 7) {
const lower = text.substring(i, i + 8).toLowerCase();
if (lower === '<nowiki>') {
inNowiki = true;
i += 8;
continue;
}
}
if (inNowiki && i < text.length - 8) {
const lower = text.substring(i, i + 9).toLowerCase();
if (lower === '</nowiki>') {
inNowiki = false;
i += 9;
continue;
}
}
// Handle pre tags
if (!inComment && !inNowiki && i < text.length - 4) {
const lower = text.substring(i, i + 5).toLowerCase();
if (lower === '<pre>') {
inPre = true;
i += 5;
continue;
}
}
if (inPre && i < text.length - 5) {
const lower = text.substring(i, i + 6).toLowerCase();
if (lower === '</pre>') {
inPre = false;
i += 6;
continue;
}
}
if (inComment || inNowiki || inPre) {
i++;
continue;
}
// Count braces
if (i < text.length - 1) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
continue;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
if (depth === 0) {
return i + 2;
}
i += 2;
continue;
}
}
i++;
}
} catch (error) {
console.error('Error finding matching braces:', error);
}
return -1;
}
/**
* Parse template parameters with proper nesting handling
*/
static parseParameters(templateText) {
console.log('=== PARSING PARAMETERS ===');
console.log('Template text:', templateText.substring(0, 200));
const params = [];
try {
if (!templateText.startsWith('{{') || !templateText.endsWith('}}')) {
console.log('Invalid template format - missing {{ or }}');
return params;
}
const inner = templateText.substring(2, templateText.length - 2);
console.log('Inner text length:', inner.length);
// Skip template name
const firstPipe = TemplateParser.findNextPipe(inner, 0);
console.log('First pipe position (after template name):', firstPipe);
if (firstPipe === -1) {
console.log('No parameters found (no pipe after template name)');
return params;
}
const templateName = inner.substring(0, firstPipe).trim();
console.log('Template name:', templateName);
let pos = firstPipe + 1;
let unnamedIndex = 1;
let paramIndex = 0;
while (pos < inner.length) {
const nextPipe = TemplateParser.findNextPipe(inner, pos);
const paramText = nextPipe === -1
? inner.substring(pos).trim()
: inner.substring(pos, nextPipe).trim();
console.log(`\nParameter ${paramIndex}:`);
console.log(` Position: ${pos} to ${nextPipe === -1 ? 'end' : nextPipe}`);
console.log(` Raw text: "${paramText.substring(0, 100)}${paramText.length > 100 ? '...' : ''}"`);
if (paramText.length > 0) {
const equalsPos = TemplateParser.findFirstEquals(paramText);
console.log(` Equals position: ${equalsPos}`);
if (equalsPos !== -1) {
const name = paramText.substring(0, equalsPos).trim();
const value = paramText.substring(equalsPos + 1).trim();
console.log(` → Named parameter: "${name}" = "${value.substring(0, 50)}${value.length > 50 ? '...' : ''}"`);
if (name.length > 0) {
params.push({
name: name,
value: value,
originalText: paramText,
isNamed: true
});
} else {
console.log(' → Skipped: empty parameter name');
}
} else {
// Unnamed parameter
console.log(` → Unnamed parameter [${unnamedIndex}]: "${paramText.substring(0, 50)}${paramText.length > 50 ? '...' : ''}"`);
params.push({
name: String(unnamedIndex),
value: paramText,
originalText: paramText,
isNamed: false
});
unnamedIndex++;
}
} else {
console.log(' → Skipped: empty parameter');
}
pos = nextPipe === -1 ? inner.length : nextPipe + 1;
paramIndex++;
}
} catch (error) {
console.error('Error parsing parameters:', error);
}
console.log(`\nTotal parameters parsed: ${params.length}`);
params.forEach((p, i) => {
console.log(` [${i}] "${p.name}" = "${p.value.substring(0, 30)}${p.value.length > 30 ? '...' : ''}"`);
});
return params;
}
/**
* Find next pipe not inside nested structures
*/
static findNextPipe(text, startPos) {
let braceDepth = 0;
let bracketDepth = 0;
let i = startPos;
try {
while (i < text.length) {
if (i < text.length - 1) {
const twoChar = text.substring(i, i + 2);
if (twoChar === '{{') {
braceDepth++;
i += 2;
continue;
} else if (twoChar === '}}') {
braceDepth--;
i += 2;
continue;
} else if (twoChar === '[[') {
bracketDepth++;
i += 2;
continue;
} else if (twoChar === ']]') {
bracketDepth--;
i += 2;
continue;
}
}
if (text[i] === '|' && braceDepth === 0 && bracketDepth === 0) {
return i;
}
i++;
}
} catch (error) {
console.error('Error finding next pipe:', error);
}
return -1;
}
/**
* Find first equals sign not inside nested structures
*/
static findFirstEquals(text) {
let braceDepth = 0;
let bracketDepth = 0;
let i = 0;
try {
while (i < text.length) {
if (i < text.length - 1) {
const twoChar = text.substring(i, i + 2);
if (twoChar === '{{') {
braceDepth++;
i += 2;
continue;
} else if (twoChar === '}}') {
braceDepth--;
i += 2;
continue;
} else if (twoChar === '[[') {
bracketDepth++;
i += 2;
continue;
} else if (twoChar === ']]') {
bracketDepth--;
i += 2;
continue;
}
}
if (text[i] === '=' && braceDepth === 0 && bracketDepth === 0) {
return i;
}
i++;
}
} catch (error) {
console.error('Error finding equals sign:', error);
}
return -1;
}
}
// ============================================================================
// DUPLICATE DETECTION
// ============================================================================
class DuplicateDetector {
static findDuplicates(params) {
console.log('\n=== FINDING DUPLICATES ===');
const paramMap = new Map();
const duplicates = [];
try {
// Group parameters by name (case-insensitive)
params.forEach((param, index) => {
const normalizedName = param.name.toLowerCase().trim();
console.log(`Processing param ${index}: "${param.name}" (normalized: "${normalizedName}")`);
if (!paramMap.has(normalizedName)) {
paramMap.set(normalizedName, []);
console.log(` → New parameter name, created group`);
} else {
console.log(` → DUPLICATE DETECTED! Adding to existing group`);
}
paramMap.get(normalizedName).push({
...param,
originalIndex: index
});
});
console.log('\n=== PARAMETER GROUPS ===');
paramMap.forEach((instances, name) => {
console.log(`Parameter "${name}": ${instances.length} instance(s)`);
instances.forEach((inst, i) => {
console.log(` [${i}] value: "${inst.value.substring(0, 50)}${inst.value.length > 50 ? '...' : ''}"`);
});
});
// Find duplicates
paramMap.forEach((instances, name) => {
if (instances.length > 1) {
const values = instances.map(p => p.value.trim());
const uniqueValues = [...new Set(values)];
console.log(`\nDUPLICATE FOUND: "${name}"`);
console.log(` Instances: ${instances.length}`);
console.log(` Unique values: ${uniqueValues.length}`);
console.log(` All same value: ${uniqueValues.length === 1}`);
duplicates.push({
name: instances[0].name, // Use original casing
instances: instances,
allSameValue: uniqueValues.length === 1,
uniqueValues: uniqueValues
});
}
});
} catch (error) {
console.error('Error finding duplicates:', error);
}
console.log(`\n=== DUPLICATE SUMMARY ===`);
console.log(`Total duplicate parameter names found: ${duplicates.length}`);
return duplicates;
}
}
// ============================================================================
// TEMPLATE FIXER
// ============================================================================
class TemplateFixer {
/**
* Remove specific parameter instances from template
* @param {string} templateText - The full template text
* @param {Array} instancesToRemove - Array of {name, value} objects to remove
*/
static removeParameterInstances(templateText, instancesToRemove) {
if (!instancesToRemove || instancesToRemove.length === 0) {
return templateText;
}
console.log('\n=== REMOVING PARAMETER INSTANCES ===');
console.log(`Instances to remove: ${instancesToRemove.length}`);
try {
let result = templateText;
// Sort by position (if available) to remove from end to start
// This prevents position shifts
const sorted = [...instancesToRemove].reverse();
sorted.forEach((inst, idx) => {
const paramName = inst.name.trim();
const paramValue = inst.value.trim();
console.log(`\nRemoving instance ${idx}:`);
console.log(` Name: "${paramName}"`);
console.log(` Value: "${paramValue.substring(0, 50)}${paramValue.length > 50 ? '...' : ''}"`);
// Build a pattern that matches this specific parameter with this specific value
// We need to be very precise here
const escapedName = escapeRegex(paramName);
const escapedValue = escapeRegex(paramValue);
// Match |paramname = value (with flexible whitespace)
// Use a more specific pattern that includes the value
const pattern = new RegExp(
'\\|\\s*' + escapedName + '\\s*=\\s*' + escapedValue + '(?=\\s*\\||\\s*\\}\\})',
'i'
);
const beforeLength = result.length;
// Only remove the first occurrence (since we're processing each instance)
result = result.replace(pattern, '');
const afterLength = result.length;
if (beforeLength === afterLength) {
console.log(` → WARNING: Pattern did not match anything!`);
console.log(` → Pattern: ${pattern}`);
} else {
console.log(` → Removed ${beforeLength - afterLength} characters`);
}
});
return result;
} catch (error) {
console.error('Error removing parameter instances:', error);
return templateText;
}
}
/**
* Rename specific parameter instance in template
* @param {string} templateText - The full template text
* @param {Object} instance - {name, value} object to rename
* @param {string} newName - New parameter name
*/
static renameParameterInstance(templateText, instance, newName) {
console.log('\n=== RENAMING PARAMETER INSTANCE ===');
console.log(`Old name: "${instance.name}"`);
console.log(`Value: "${instance.value.substring(0, 50)}${instance.value.length > 50 ? '...' : ''}"`);
console.log(`New name: "${newName}"`);
try {
const paramName = instance.name.trim();
const paramValue = instance.value.trim();
// Find the exact string |paramname = value
// We'll search for it directly to ensure we match the right instance
const escapedName = escapeRegex(paramName);
const escapedValue = escapeRegex(paramValue);
// Build pattern - match parameter name and exact value
// No case-insensitive flag, so values must match exactly
const pattern = new RegExp(
'(\\|\\s*)(' + escapedName + ')(\\s*=\\s*)(' + escapedValue + ')(?=\\s*\\||\\s*\\}\\})'
);
console.log(`Pattern: ${pattern}`);
// Replace only the first occurrence
const result = templateText.replace(pattern, function(match, prefix, oldName, equals, value) {
console.log(` → Match found: "${match.substring(0, 100)}"`);
return `${prefix}${newName}${equals}${value}`;
});
if (result === templateText) {
console.log(` → WARNING: No match found for rename!`);
} else {
console.log(` → Successfully renamed`);
}
return result;
} catch (error) {
console.error('Error renaming parameter instance:', error);
return templateText;
}
}
/**
* Legacy method for backward compatibility
*/
static removeParameters(templateText, paramNamesToRemove) {
if (!paramNamesToRemove || paramNamesToRemove.length === 0) {
return templateText;
}
try {
const removeSet = new Set(paramNamesToRemove.map(n => n.toLowerCase().trim()));
let result = templateText;
// Remove each parameter
removeSet.forEach(paramName => {
const escaped = escapeRegex(paramName);
// Match |paramname=value where value can span multiple lines
const pattern = new RegExp(
'\\|\\s*' + escaped + '\\s*=(?:[^|{}]|\\{[^{}]*\\})*?(?=\\s*\\||\\s*\\}\\})',
'gi'
);
result = result.replace(pattern, '');
});
return result;
} catch (error) {
console.error('Error removing parameters:', error);
return templateText;
}
}
}
// ============================================================================
// UI COMPONENTS
// ============================================================================
class UI {
static init() {
try {
UI.addToolbarLink();
UI.createMessageArea();
} catch (error) {
console.error('Error initializing UI:', error);
}
}
static addToolbarLink() {
if (typeof mw === 'undefined' || !mw.loader) return;
mw.loader.using(['mediawiki.util']).done(() => {
try {
const portletLink = mw.util.addPortletLink(
'p-tb',
'#',
CONFIG.buttonText,
't-findargdups'
);
if (portletLink) {
$(portletLink).click((e) => {
e.preventDefault();
DuplicateFinder.run();
});
}
} catch (error) {
console.error('Error adding toolbar link:', error);
}
});
}
static createMessageArea() {
if ($('#duplicate-params-message').length) return;
try {
$('body').append(
$('<div>')
.attr('id', 'duplicate-params-message')
.css({
'position': 'fixed',
'bottom': '20px',
'right': '20px',
'padding': '12px 16px',
'background-color': '#f8f9fa',
'border': '1px solid #a2a9b1',
'border-radius': '4px',
'box-shadow': '0 2px 8px rgba(0, 0, 0, 0.15)',
'z-index': '10000',
'display': 'none',
'max-width': '400px',
'font-family': 'sans-serif',
'font-size': '14px'
})
);
} catch (error) {
console.error('Error creating message area:', error);
}
}
static showMessage(message, type = 'info') {
console.log(`[UI Message] ${type}: ${message}`);
try {
const colors = {
success: '#d4edda',
error: '#f8d7da',
warning: '#fff3cd',
info: '#d1ecf1'
};
const $message = $('#duplicate-params-message');
if ($message.length) {
$message
.text(message)
.css('background-color', colors[type] || colors.info)
.fadeIn()
.delay(5000)
.fadeOut();
}
if (CONFIG.showResultsBox) {
UI.addResultsBox(message);
}
} catch (error) {
console.error('Error showing message:', error);
}
}
static addResultsBox(text) {
try {
const summaryLabel = document.getElementById('wpSummaryLabel');
if (!summaryLabel) return;
const parentDiv = summaryLabel.parentNode;
if (!parentDiv) return;
let resultsBox = document.getElementById('FindArgDupsResultsBox');
if (!resultsBox) {
resultsBox = document.createElement('div');
resultsBox.id = 'FindArgDupsResultsBox';
parentDiv.insertBefore(resultsBox, parentDiv.firstChild);
}
resultsBox.innerHTML = '';
const messageDiv = document.createElement('div');
messageDiv.className = 'FindArgDupsResultsBox';
messageDiv.style.cssText = 'max-height: 6em; overflow: auto; padding: 8px; ' +
'border: 1px solid #aaa; background-color: #fffacd; margin-bottom: 10px; ' +
'border-radius: 3px; font-family: monospace; font-size: 0.9em;';
messageDiv.textContent = text;
resultsBox.appendChild(messageDiv);
} catch (error) {
console.error('Error adding results box:', error);
}
}
static clearResultsBox() {
try {
const resultsBox = document.getElementById('FindArgDupsResultsBox');
if (resultsBox) {
resultsBox.innerHTML = '';
}
} catch (error) {
console.error('Error clearing results box:', error);
}
}
static showConflictDialog(duplicates, templateText, callback) {
try {
// Use MediaWiki OOUI which is always available
mw.loader.using(['oojs-ui-core', 'oojs-ui-windows', 'oojs-ui-widgets']).done(() => {
UI.showOOUIDialog(duplicates, templateText, callback);
}).fail((error) => {
console.error('Failed to load OOUI:', error);
// Fallback to custom modal
UI.showCustomModal(duplicates, templateText, callback);
});
} catch (error) {
console.error('Error showing conflict dialog:', error);
// Ultimate fallback to simple confirmation
if (confirm(`Parameter "${duplicates[0].name}" has multiple different values. Keep the first one?`)) {
callback({action: 'keep', selectedIndex: 0});
} else {
callback({action: 'skip'});
}
}
}
static showOOUIDialog(duplicates, templateText, callback) {
try {
const instances = duplicates[0].instances;
const textInputs = [];
const checkboxes = [];
// Create message dialog
function ConflictDialog(config) {
ConflictDialog.super.call(this, config);
}
OO.inheritClass(ConflictDialog, OO.ui.ProcessDialog);
ConflictDialog.static.name = 'conflictDialog';
ConflictDialog.static.title = 'Duplicate Parameter Found';
ConflictDialog.static.actions = [
{
action: 'skip',
label: 'Skip',
flags: 'safe'
},
{
action: 'save',
label: 'Save',
flags: ['primary', 'progressive']
}
];
ConflictDialog.prototype.initialize = function() {
ConflictDialog.super.prototype.initialize.call(this);
const messageLabel = new OO.ui.LabelWidget({
label: new OO.ui.HtmlSnippet(
`<div style="margin-bottom: 15px; line-height: 1.6;">` +
`Parameter <strong>"${escapeHtml(duplicates[0].name)}"</strong> has ` +
`${instances.length} instances with different values.<br>` +
`Edit parameter names to keep multiple values, or uncheck to remove.</div>`
)
});
const fieldset = new OO.ui.FieldsetLayout({
label: 'Parameter Instances'
});
instances.forEach((inst, i) => {
const checkbox = new OO.ui.CheckboxInputWidget({
selected: true
});
checkboxes.push(checkbox);
const input = new OO.ui.TextInputWidget({
value: inst.name, // Just use the original name, user will edit as needed
disabled: false
});
textInputs.push(input);
// Disable input when checkbox is unchecked
checkbox.on('change', function(selected) {
input.setDisabled(!selected);
if (!selected) {
input.$element.css('opacity', '0.5');
} else {
input.$element.css('opacity', '1');
}
});
const valuePreview = inst.value.substring(0, 100) + (inst.value.length > 100 ? '...' : '');
// Create a horizontal layout with checkbox and text input
const $container = $('<div>').css({
'margin-bottom': '12px',
'padding': '12px',
'background': '#f8f9fa',
'border': '1px solid #c8ccd1',
'border-radius': '4px'
});
const $topRow = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-bottom': '8px'
});
const $checkboxContainer = $('<div>').css({
'flex-shrink': '0'
});
$checkboxContainer.append(checkbox.$element);
const $inputContainer = $('<div>').css({
'flex': '1',
'min-width': '0'
});
$inputContainer.append(input.$element);
$topRow.append($checkboxContainer, $inputContainer);
const $valueDiv = $('<div>').css({
'padding': '8px',
'background': 'white',
'border': '1px solid #ddd',
'border-radius': '3px',
'font-family': 'monospace',
'font-size': '12px',
'color': '#555',
'overflow-x': 'auto',
'word-break': 'break-all'
}).text('= ' + valuePreview);
$container.append($topRow, $valueDiv);
const field = new OO.ui.FieldLayout($container, {
align: 'top'
});
fieldset.addItems([field]);
});
// Create template preview
const templatePreview = $('<div>')
.css({
'padding': '10px',
'background': '#f8f9fa',
'border': '1px solid #c8ccd1',
'border-radius': '3px',
'max-height': '100px',
'overflow': 'auto',
'font-family': 'monospace',
'font-size': '11px',
'white-space': 'pre-wrap',
'word-break': 'break-all',
'margin-top': '15px'
})
.text(templateText.substring(0, 300) + (templateText.length > 300 ? '...' : ''));
const templateLabel = new OO.ui.LabelWidget({
label: new OO.ui.HtmlSnippet('<strong style="font-size: 13px;">Template context:</strong>')
});
const content = new OO.ui.PanelLayout({
padded: true,
expanded: false,
scrollable: true
});
content.$element.append(
messageLabel.$element,
fieldset.$element,
templateLabel.$element,
templatePreview
);
this.$body.append(content.$element);
};
ConflictDialog.prototype.getBodyHeight = function() {
return Math.min(600, 250 + instances.length * 90);
};
ConflictDialog.prototype.getActionProcess = function(action) {
const dialog = this;
if (action === 'save') {
return new OO.ui.Process(function() {
const renames = [];
textInputs.forEach((input, i) => {
const isChecked = checkboxes[i].isSelected();
renames.push({
originalName: instances[i].name,
originalValue: instances[i].value,
newName: isChecked ? input.getValue().trim() : '',
instance: instances[i]
});
});
console.log('User dialog result:', renames);
// Validate: check for duplicate new names (only among checked items)
const newNames = renames
.filter(r => r.newName)
.map(r => r.newName.toLowerCase());
const duplicateNewNames = newNames.filter((name, i) => newNames.indexOf(name) !== i);
if (duplicateNewNames.length > 0) {
OO.ui.alert('Error: You have duplicate parameter names. Each parameter must have a unique name.');
return;
}
callback({action: 'rename', renames: renames});
dialog.close({ action: action });
});
} else if (action === 'skip') {
return new OO.ui.Process(function() {
callback({action: 'skip'});
dialog.close({ action: action });
});
}
return ConflictDialog.super.prototype.getActionProcess.call(this, action);
};
// Create window manager and open dialog
const windowManager = new OO.ui.WindowManager();
$('body').append(windowManager.$element);
const dialog = new ConflictDialog({
size: 'large'
});
windowManager.addWindows([dialog]);
windowManager.openWindow(dialog);
} catch (error) {
console.error('Error showing OOUI dialog:', error);
UI.showCustomModal(duplicates, templateText, callback);
}
}
static showCustomModal(duplicates, templateText, callback) {
try {
const instances = duplicates[0].instances;
// Create custom modal overlay
const overlay = document.createElement('div');
overlay.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 100000; display: flex; align-items: center; justify-content: center;';
const modal = document.createElement('div');
modal.style.cssText = 'background: white; border-radius: 8px; padding: 24px; max-width: 700px; max-height: 85vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0,0,0,0.3); font-family: sans-serif;';
// Build modal content
let html = `
<h3 style="margin: 0 0 16px 0; font-size: 18px; color: #202122;">Duplicate Parameter Found</h3>
<p style="margin: 0 0 16px 0; line-height: 1.5;">Parameter <strong>"${escapeHtml(duplicates[0].name)}"</strong> has ${instances.length} instances with different values.<br>Edit parameter names to keep multiple values, or uncheck to remove.</p>
<div style="max-height: 450px; overflow-y: auto; margin-bottom: 16px;">
`;
instances.forEach((inst, i) => {
const valuePreview = inst.value.substring(0, 120);
html += `
<div style="margin-bottom: 12px; padding: 12px; background: #f8f9fa; border: 1px solid #c8ccd1; border-radius: 4px;">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
<input
type="checkbox"
class="param-checkbox"
data-index="${i}"
checked
style="width: 18px; height: 18px; cursor: pointer; flex-shrink: 0;">
<input
type="text"
class="param-input"
data-index="${i}"
value="${escapeHtml(inst.name)}"
style="flex: 1; padding: 8px; border: 1px solid #a2a9b1; border-radius: 3px; font-size: 14px; font-family: monospace;">
</div>
<div style="padding: 8px; background: white; border: 1px solid #ddd; border-radius: 3px; font-family: monospace; font-size: 12px; color: #555; overflow-x: auto; word-break: break-all;">
= ${escapeHtml(valuePreview)}${inst.value.length > 120 ? '...' : ''}
</div>
</div>
`;
});
html += `
</div>
<div style="padding: 10px; background: #f8f9fa; border: 1px solid #c8ccd1; border-radius: 4px; margin-bottom: 16px; max-height: 100px; overflow: auto;">
<strong style="font-size: 13px;">Template context:</strong><br>
<code style="font-size: 11px; word-break: break-all; color: #202122;">${escapeHtml(templateText.substring(0, 300))}${templateText.length > 300 ? '...' : ''}</code>
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end;">
<button id="skip-btn" style="padding: 8px 16px; background: white; border: 1px solid #a2a9b1; border-radius: 2px; cursor: pointer; font-size: 14px;">Skip</button>
<button id="save-btn" style="padding: 8px 16px; background: #36c; color: white; border: none; border-radius: 2px; cursor: pointer; font-size: 14px; font-weight: 500;">Save</button>
</div>
`;
modal.innerHTML = html;
overlay.appendChild(modal);
document.body.appendChild(overlay);
// Add checkbox change handlers to enable/disable inputs
const checkboxes = modal.querySelectorAll('.param-checkbox');
const inputs = modal.querySelectorAll('.param-input');
checkboxes.forEach((checkbox, i) => {
checkbox.addEventListener('change', function() {
inputs[i].disabled = !this.checked;
inputs[i].style.opacity = this.checked ? '1' : '0.5';
inputs[i].style.backgroundColor = this.checked ? 'white' : '#f0f0f0';
});
});
// Save button handler
modal.querySelector('#save-btn').addEventListener('click', () => {
const renames = [];
inputs.forEach((input, i) => {
const isChecked = checkboxes[i].checked;
renames.push({
originalName: instances[i].name,
originalValue: instances[i].value,
newName: isChecked ? input.value.trim() : '',
instance: instances[i]
});
});
// Validate: check for duplicate new names (only among checked items)
const newNames = renames
.filter(r => r.newName)
.map(r => r.newName.toLowerCase());
const duplicateNewNames = newNames.filter((name, i) => newNames.indexOf(name) !== i);
if (duplicateNewNames.length > 0) {
alert('Error: You have duplicate parameter names. Each parameter must have a unique name.');
return;
}
document.body.removeChild(overlay);
callback({action: 'rename', renames: renames});
});
// Skip button handler
modal.querySelector('#skip-btn').addEventListener('click', () => {
document.body.removeChild(overlay);
callback({action: 'skip'});
});
// Close on overlay click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
document.body.removeChild(overlay);
callback({action: 'skip'});
}
});
// Close on Escape key
const escapeHandler = (e) => {
if (e.key === 'Escape') {
if (document.body.contains(overlay)) {
document.body.removeChild(overlay);
callback({action: 'skip'});
}
document.removeEventListener('keydown', escapeHandler);
}
};
document.addEventListener('keydown', escapeHandler);
// Focus first input
setTimeout(() => {
const firstInput = modal.querySelector('.param-input');
if (firstInput) firstInput.focus();
}, 100);
} catch (error) {
console.error('Error showing custom modal:', error);
// Ultimate fallback
if (confirm(`Parameter "${duplicates[0].name}" has multiple different values. Keep the first one?`)) {
callback({action: 'keep', selectedIndex: 0});
} else {
callback({action: 'skip'});
}
}
}
}
// ============================================================================
// MAIN DUPLICATE FINDER
// ============================================================================
class DuplicateFinder {
static run() {
console.log('\n========================================');
console.log('STARTING DUPLICATE PARAMETER CHECK');
console.log('========================================\n');
try {
const textbox = document.getElementById('wpTextbox1');
if (!textbox) {
UI.showMessage('Edit textbox not found.', 'error');
return;
}
const originalContent = textbox.value;
console.log(`Content length: ${originalContent.length} characters`);
const templates = TemplateParser.extractTemplates(originalContent);
if (templates.length === 0) {
UI.showMessage(CONFIG.noneFoundMessage, 'info');
return;
}
// Limit templates to prevent browser hang
const templatesToCheck = templates.slice(0, CONFIG.maxTemplatesPerRun);
if (templates.length > CONFIG.maxTemplatesPerRun) {
console.log(`\nLimited to first ${CONFIG.maxTemplatesPerRun} templates out of ${templates.length} total`);
}
const problemTemplates = [];
let totalDuplicates = 0;
// Check each template
templatesToCheck.forEach((template, index) => {
console.log(`\n--- Checking template ${index + 1} of ${templatesToCheck.length} (level ${template.level}) ---`);
const params = TemplateParser.parseParameters(template.text);
if (params.length === 0) {
console.log('No parameters found, skipping');
return;
}
const duplicates = DuplicateDetector.findDuplicates(params);
if (duplicates.length > 0) {
console.log(`✓ Template has ${duplicates.length} duplicate parameter(s)`);
problemTemplates.push({
template: template,
params: params,
duplicates: duplicates
});
totalDuplicates += duplicates.length;
} else {
console.log('✓ No duplicates found in this template');
}
});
console.log('\n========================================');
console.log(`SCAN COMPLETE`);
console.log(`Templates checked: ${templatesToCheck.length}`);
console.log(`Templates with duplicates: ${problemTemplates.length}`);
console.log(`Total duplicate parameters: ${totalDuplicates}`);
console.log('========================================\n');
if (problemTemplates.length === 0) {
UI.showMessage(CONFIG.noneFoundMessage, 'success');
UI.clearResultsBox();
return;
}
// Highlight first problematic template
if (problemTemplates.length > 0) {
const firstTemplate = problemTemplates[0].template;
textbox.setSelectionRange(firstTemplate.start, firstTemplate.end);
textbox.focus();
console.log(`Highlighted first problematic template at position ${firstTemplate.start}-${firstTemplate.end}`);
}
// Process templates
DuplicateFinder.processTemplates(problemTemplates, originalContent, textbox);
} catch (error) {
console.error('CRITICAL ERROR in run():', error);
UI.showMessage('An error occurred: ' + error.message, 'error');
}
}
static processTemplates(problemTemplates, currentContent, textbox) {
console.log('\n=== PROCESSING TEMPLATES ===');
try {
let autoRemovedCount = 0;
let needUserInputCount = 0;
let newContent = currentContent;
let offset = 0;
// First pass: auto-fix identical duplicates
problemTemplates.forEach((item, idx) => {
console.log(`\nProcessing template ${idx + 1}:`);
const instancesToRemove = [];
let hasConflicts = false;
item.duplicates.forEach(dup => {
if (dup.allSameValue) {
console.log(` Parameter "${dup.name}": ${dup.instances.length} identical values - auto-removing duplicates`);
// Remove all but the first instance
for (let i = 1; i < dup.instances.length; i++) {
instancesToRemove.push({
name: dup.instances[i].name,
value: dup.instances[i].value
});
}
autoRemovedCount += dup.instances.length - 1;
} else {
console.log(` Parameter "${dup.name}": ${dup.instances.length} different values - needs user input`);
hasConflicts = true;
needUserInputCount++;
}
});
if (instancesToRemove.length > 0) {
const fixedTemplate = TemplateFixer.removeParameterInstances(item.template.text, instancesToRemove);
const start = item.template.start + offset;
const end = item.template.end + offset;
newContent = newContent.substring(0, start) + fixedTemplate + newContent.substring(end);
offset += fixedTemplate.length - item.template.text.length;
console.log(` Applied auto-fixes, offset is now ${offset}`);
}
item.hasConflicts = hasConflicts;
});
// Apply auto-fixes
if (autoRemovedCount > 0) {
console.log(`\nApplying ${autoRemovedCount} auto-fixes to textbox`);
textbox.value = newContent;
DuplicateFinder.updateEditSummary();
UI.showMessage(
`Auto-removed ${autoRemovedCount} duplicate parameter(s) with identical values.`,
'success'
);
}
// Handle conflicts that need user input
if (needUserInputCount > 0) {
console.log(`\n${needUserInputCount} conflict(s) need user input`);
const conflictTemplates = problemTemplates.filter(item => item.hasConflicts);
// Process conflicts sequentially
DuplicateFinder.processConflictsSequentially(conflictTemplates, 0, textbox);
}
} catch (error) {
console.error('Error processing templates:', error);
UI.showMessage('Error processing templates: ' + error.message, 'error');
}
}
static processConflictsSequentially(conflictTemplates, index, textbox) {
console.log(`\n=== PROCESSING CONFLICT ${index + 1} of ${conflictTemplates.length} ===`);
if (index >= conflictTemplates.length) {
console.log('\n✓ All conflicts resolved!');
UI.showMessage('All conflicts resolved!', 'success');
return;
}
try {
const item = conflictTemplates[index];
const conflictDuplicates = item.duplicates.filter(dup => !dup.allSameValue);
if (conflictDuplicates.length === 0) {
console.log('No conflicts in this template, moving to next');
// No conflicts in this template, move to next
DuplicateFinder.processConflictsSequentially(conflictTemplates, index + 1, textbox);
return;
}
// Show dialog for first conflict in this template
const currentConflict = conflictDuplicates[0];
console.log(`Showing dialog for parameter "${currentConflict.name}"`);
UI.showConflictDialog(
[currentConflict],
item.template.text,
(result) => {
console.log(`Dialog result:`, result);
if (result.action === 'rename') {
// User chose to rename/remove parameters
let currentContent = textbox.value;
let templateStart = currentContent.indexOf(item.template.text);
if (templateStart !== -1) {
let fixedTemplate = item.template.text;
let renameCount = 0;
console.log('Applying user changes:');
// Apply renames and removals
// Process in order - the value match ensures we get the right instance
result.renames.forEach((rename, i) => {
console.log(` [${i}] "${rename.originalName}" → "${rename.newName || '(remove)'}"`);
if (rename.newName) {
// Rename this instance if the name actually changed
if (rename.newName.toLowerCase() !== rename.originalName.toLowerCase()) {
const beforeRename = fixedTemplate;
fixedTemplate = TemplateFixer.renameParameterInstance(
fixedTemplate,
rename.instance,
rename.newName
);
if (fixedTemplate !== beforeRename) {
renameCount++;
}
}
} else {
// Remove this instance (empty newName means unchecked)
fixedTemplate = TemplateFixer.removeParameterInstances(
fixedTemplate,
[{name: rename.instance.name, value: rename.instance.value}]
);
}
});
const templateEnd = templateStart + item.template.text.length;
textbox.value = currentContent.substring(0, templateStart) +
fixedTemplate +
currentContent.substring(templateEnd);
// Update template text for next conflict
item.template.text = fixedTemplate;
DuplicateFinder.updateEditSummary();
console.log(`Applied ${renameCount} rename(s) and ${result.renames.filter(r => !r.newName).length} removal(s)`);
}
} else {
console.log('User skipped this conflict');
}
// Move to next template
setTimeout(() => {
DuplicateFinder.processConflictsSequentially(conflictTemplates, index + 1, textbox);
}, 100);
}
);
} catch (error) {
console.error('Error processing conflict:', error);
// Continue to next template
DuplicateFinder.processConflictsSequentially(conflictTemplates, index + 1, textbox);
}
}
static updateEditSummary() {
try {
const summaryField = document.getElementsByName('wpSummary')[0];
if (!summaryField) return;
if (summaryField.value.indexOf(CONFIG.summaryText) === -1) {
if (summaryField.value.match(/[^\*\/\s][^\/\s]?\s*$/)) {
summaryField.value += '; ' + CONFIG.summaryText;
} else {
summaryField.value += CONFIG.summaryText;
}
console.log('Updated edit summary');
}
} catch (error) {
console.error('Error updating edit summary:', error);
}
}
}
// ============================================================================
// INITIALIZATION
// ============================================================================
function initialize() {
try {
// Only run on edit pages
if (typeof mw === 'undefined') return;
const action = mw.config.get('wgAction');
const namespace = mw.config.get('wgNamespaceNumber');
if (action !== 'edit' && namespace !== -1) return;
// Wait for jQuery
if (typeof $ === 'undefined' || typeof jQuery === 'undefined') {
setTimeout(initialize, 100);
return;
}
$(document).ready(() => {
UI.init();
console.log('[DuplicateParameters] Initialized successfully');
});
} catch (error) {
console.error('[DuplicateParameters] Initialization error:', error);
}
}
// Start initialization
initialize();
})();
// </nowiki>
Content Disclaimer
Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.
- The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
- There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
- It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
- Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
- Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.