User:Polygnotus/Scripts/DeduplicateReferences.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.
This code will be executed when previewing this page.
This user script seems to have a documentation page at User:Polygnotus/Scripts/DeduplicateReferences.
// <nowiki>
// Only exact duplicates
// Tries to come up with a name for the reference
// Only runs when editing the current version of the article
// Function to deduplicate references in Wikipedia articles
function deduplicateReferences() {
console.log('Starting deduplication process...');
// Get the edit textarea and summary input
const editTextarea = document.getElementById('wpTextbox1');
const summaryInput = document.getElementById('wpSummary');
if (!editTextarea || !summaryInput) {
console.log('Edit textarea or summary input not found');
return;
}
let content = editTextarea.value;
console.log('Content length:', content.length);
// Use a robust approach to find ref tags
// First find all <ref> start positions, then manually parse each one
function findAllReferences(text) {
const refs = [];
let pos = 0;
while (pos < text.length) {
const refStart = text.indexOf('<ref', pos);
if (refStart === -1) break;
// Find the end of the opening tag
const tagEnd = text.indexOf('>', refStart);
if (tagEnd === -1) break;
const openingTag = text.substring(refStart, tagEnd + 1);
// Check if it's self-closing
if (openingTag.endsWith('/>')) {
refs.push(openingTag);
pos = tagEnd + 1;
} else {
// Find the closing </ref>
const closeStart = text.indexOf('</ref>', tagEnd);
if (closeStart === -1) {
pos = tagEnd + 1;
continue;
}
const fullRef = text.substring(refStart, closeStart + 6);
refs.push(fullRef);
pos = closeStart + 6;
}
}
return refs;
}
// Object to store all references
const allRefs = {};
// Set to store all used reference names
const usedNames = new Set();
// Blacklist of reference names to ignore
const blacklist = [
"doi_org",
"jstor_org",
"amazon_com",
"books_google_com",
"web_archive_org",
"worldcat_org",
"dx_doi_org",
"patents_google_com",
"cite_journal",
"cite_book",
"cite_web",
"cite_news",
"cite_magazine",
"cite_newspaper",
"cite_thesis",
"cite_conference",
"cite_encyclopedia",
"cite_album_notes",
"cite_comic",
"cite_court",
"cite_act",
"cite_episode",
"cite_mailing_list",
"cite_map",
"cite_newsgroup",
"cite_patent",
"cite_press_release",
"cite_report",
"cite_video_game",
"citation"
];
// Function to extract domain name from URL
function extractDomain(url) {
try {
let domain = new URL(url).hostname;
domain = domain.replace(/^www\./, '');
return domain === 'archive.org' ? extractDomain(url.split('archive.org/web/')[1]) : domain;
} catch (e) {
return null;
}
}
// Function to extract title from cite templates
function extractTitleFromCiteTemplate(ref) {
// Look for title parameter in cite templates
const titleMatch = ref.match(/\|\s*title\s*=\s*([^|{}]+?)(?:\s*\||$)/i);
if (titleMatch) {
let title = titleMatch[1].trim();
// Remove any remaining markup
title = title.replace(/<[^>]*>/g, '').replace(/\[\[([^\]]*)\]\]/g, '$1');
// Extract first 3 words
const words = title.match(/\b[A-Za-z0-9]+\b/g);
if (words && words.length > 0) {
return words.slice(0, 3).join('').toLowerCase();
}
}
return null;
}
// Function to generate a unique name for the reference
function generateUniqueName(ref) {
// Check if this is a cite template and try to extract title (case insensitive)
if (ref.toLowerCase().includes('{{cite') || ref.toLowerCase().includes('{{citation')) {
const titleName = extractTitleFromCiteTemplate(ref);
if (titleName) {
let uniqueName = titleName;
let counter = 1;
while (usedNames.has(uniqueName)) {
uniqueName = `${titleName}_${counter}`;
counter++;
}
usedNames.add(uniqueName);
return uniqueName;
}
}
// Try to extract meaningful text from the reference
const textContent = ref.replace(/<[^>]*>/g, '').trim();
// Look for URLs first
const urlMatch = ref.match(/https?:\/\/[^\s<>"]+/i);
if (urlMatch) {
const domain = extractDomain(urlMatch[0]);
if (domain && !blacklist.includes(domain.replace(/\./g, '_'))) {
let baseName = domain.replace(/\./g, '_');
let uniqueName = baseName;
let counter = 1;
while (usedNames.has(uniqueName)) {
uniqueName = `${baseName}_${counter}`;
counter++;
}
usedNames.add(uniqueName);
return uniqueName;
}
}
// If no URL, try to create name from content
if (textContent) {
// Extract first 3 meaningful words
const words = textContent.match(/\b[A-Za-z0-9]+\b/g);
if (words && words.length > 0) {
let baseName = words.slice(0, 3).join('_').toLowerCase();
let uniqueName = baseName;
let counter = 1;
while (usedNames.has(uniqueName)) {
uniqueName = `${baseName}_${counter}`;
counter++;
}
usedNames.add(uniqueName);
return uniqueName;
}
}
// Fallback to generic name
let baseName = 'ref';
let uniqueName = baseName;
let counter = 1;
while (usedNames.has(uniqueName)) {
uniqueName = `${baseName}_${counter}`;
counter++;
}
usedNames.add(uniqueName);
return uniqueName;
}
// Function to extract existing name from a reference
// Handles double-quoted, single-quoted, and unquoted names,
// including names that contain apostrophes (e.g. name="Crain's 2009")
// Returns null for empty, whitespace-only, or bare 'ref' names so
// those refs are treated as unnamed and eligible for deduplication
function extractExistingName(ref) {
const nameMatch = ref.match(/name\s*=\s*"([^"]+)"|name\s*=\s*'([^']+)'|name\s*=\s*([^\s/>]+)/i);
const name = nameMatch ? (nameMatch[1] || nameMatch[2] || nameMatch[3]) : null;
if (!name || !name.trim() || name.trim().toLowerCase() === 'ref') return null;
return name;
}
// Function to create a reference tag
function createRefTag(name, content = null) {
if (content) {
return `<ref name="${name}">${content}</ref>`;
} else {
return `<ref name="${name}" />`;
}
}
// Function to check if a reference is blacklisted
function isBlacklisted(ref) {
const name = extractExistingName(ref);
return name && blacklist.includes(name);
}
// Function to normalize reference content for comparison
// Handles double-quoted, single-quoted, and unquoted names,
// including names that contain apostrophes (e.g. name="Crain's 2009")
function normalizeRef(ref) {
return ref.replace(/\s*name\s*=\s*"[^"]+"|name\s*=\s*'[^']+'|name\s*=\s*[^\s/>]+/i, '')
.replace(/\s*\|\s*/g, '|') // normalize spacing around pipe characters
.replace(/\s+/g, ' ')
.trim();
}
console.log('Starting first pass - collecting references...');
// First pass: collect all references and used names
const matches = findAllReferences(content);
console.log('Found', matches.length, 'reference tags');
// Debug: Show first few matches
console.log('First 5 matches:', matches.slice(0, 5).map(m => m.substring(0, 100) + '...'));
matches.forEach(match => {
if (!isBlacklisted(match)) {
const normalizedRef = normalizeRef(match);
const existingName = extractExistingName(match);
if (existingName) {
usedNames.add(existingName);
// Skip named references - they should never be deduplicated or modified
return;
}
// Only process unnamed references for deduplication
if (allRefs[normalizedRef]) {
allRefs[normalizedRef].count++;
allRefs[normalizedRef].instances.push(match);
} else {
allRefs[normalizedRef] = {
count: 1,
name: null, // Ensure unnamed refs don't get existing names
firstOccurrence: match,
instances: [match]
};
}
}
});
console.log('Reference analysis:', Object.keys(allRefs).map(key => ({
ref: key.substring(0, 50) + '...',
count: allRefs[key].count,
hasName: !!allRefs[key].name
})));
// Find duplicates (only among unnamed references)
const duplicates = Object.keys(allRefs).filter(key =>
allRefs[key].count > 1 && !allRefs[key].name // Only unnamed duplicates
);
console.log('Found', duplicates.length, 'duplicate unnamed reference groups');
// Second pass: replace duplicates with named references
let deduplicatedCount = 0;
let processedRefs = new Set();
console.log('Starting second pass - replacing duplicates...');
// Process each duplicate group (only unnamed references)
duplicates.forEach(normalizedRef => {
const refInfo = allRefs[normalizedRef];
// Double-check this is an unnamed reference group
if (refInfo.name) {
console.log('WARNING: Skipping named reference group:', refInfo.name);
return;
}
// Generate a name for this duplicate reference
const generatedName = generateUniqueName(refInfo.firstOccurrence);
if (generatedName && !blacklist.includes(generatedName)) {
refInfo.name = generatedName;
console.log('Generated name for duplicate group:', generatedName);
// Extract content for the first occurrence
const contentMatch = refInfo.firstOccurrence.match(/<ref[^>]*>([\s\S]*?)<\/ref>/i);
if (contentMatch) {
refInfo.firstOccurrence = createRefTag(generatedName, contentMatch[1]);
}
// Replace all instances
refInfo.instances.forEach((instance, index) => {
if (index === 0) {
// First occurrence - replace with named ref
content = content.replace(instance, refInfo.firstOccurrence);
console.log('Replaced first occurrence with named ref');
} else {
// Subsequent occurrences - replace with short ref
content = content.replace(instance, createRefTag(refInfo.name));
deduplicatedCount++;
console.log('Replaced duplicate with short ref:', createRefTag(refInfo.name));
}
});
}
});
console.log('Deduplication complete. Count:', deduplicatedCount);
// Update the textarea with the deduplicated content
if (deduplicatedCount > 0) {
editTextarea.value = content;
// Add edit summary
let currentSummary = summaryInput.value;
let deduplicationSummary = `[[WP:NAMEDREFS|Deduplicated]] ${deduplicatedCount} reference${deduplicatedCount > 1 ? 's' : ''} using [[User:Polygnotus/Scripts/DeduplicateReferences|DeduplicateReferences]]`;
summaryInput.value = currentSummary ? `${currentSummary} • ${deduplicationSummary}` : deduplicationSummary;
// Check minor edit if available
const minorEditCheckbox = document.querySelector('#wpMinoredit, input[name="wpMinoredit"]');
if (minorEditCheckbox) {
minorEditCheckbox.checked = true;
}
console.log('Successfully deduplicated', deduplicatedCount, 'references');
alert(`Successfully deduplicated ${deduplicatedCount} reference${deduplicatedCount > 1 ? 's' : ''}!`);
} else {
//console.log('No duplicates found to deduplicate');
//alert('No duplicate references found to deduplicate.');
}
}
// Function to check if we're editing the current version (not an old revision)
function isEditingCurrentVersion() {
// Check if there's an oldid parameter in the URL
const urlParams = new URLSearchParams(window.location.search);
const oldid = urlParams.get('oldid');
// If there's an oldid parameter, we're editing an old revision
if (oldid) {
console.log('Editing old revision (oldid=' + oldid + '), skipping deduplication');
return false;
}
// Check if we're in diff view
const diff = urlParams.get('diff');
if (diff) {
console.log('In diff view, skipping deduplication');
return false;
}
// Check for section editing of old revisions
const section = urlParams.get('section');
if (section && oldid) {
console.log('Editing section of old revision, skipping deduplication');
return false;
}
// Additional check: look for revision warning messages
const revisionWarning = document.querySelector('.mw-revision-warning, .mw-editnotice-base');
if (revisionWarning && revisionWarning.textContent.toLowerCase().includes('old revision')) {
console.log('Old revision warning detected, skipping deduplication');
return false;
}
console.log('Editing current version - deduplication allowed');
return true;
}
// Function to check if the edit textarea is ready
function isEditTextareaReady() {
const editTextarea = document.getElementById('wpTextbox1');
const summaryInput = document.getElementById('wpSummary');
return editTextarea && editTextarea.value && summaryInput;
}
// Function to run deduplication when everything is ready
function runDeduplicationWhenReady() {
console.log('Checking if ready...');
if (isEditTextareaReady()) {
console.log('Ready! Checking if editing current version...');
if (isEditingCurrentVersion()) {
console.log('Editing current version - running deduplication...');
deduplicateReferences();
} else {
console.log('Not editing current version - skipping deduplication');
}
} else {
console.log('Not ready yet, retrying...');
setTimeout(runDeduplicationWhenReady, 100);
}
}
// Add a button to manually trigger deduplication
function addDeduplicationButton() {
const toolbar = document.querySelector('.wikiEditor-ui-toolbar') ||
document.querySelector('#wpTextbox1').parentNode;
if (toolbar && !document.getElementById('dedupe-button')) {
const button = document.createElement('button');
button.id = 'dedupe-button';
button.type = 'button';
button.textContent = 'Deduplicate References';
button.style.margin = '5px';
// Only enable button if editing current version
if (isEditingCurrentVersion()) {
button.onclick = deduplicateReferences;
console.log('Added deduplication button (enabled)');
} else {
button.disabled = true;
button.title = 'Deduplication only works when editing the current version of an article';
button.onclick = () => alert('Deduplication only works when editing the current version of an article, not old revisions.');
console.log('Added deduplication button (disabled - old revision)');
}
toolbar.appendChild(button);
}
}
// Run the deduplication when the edit page is fully loaded
if (typeof mw !== 'undefined') {
const action = mw.config.get('wgAction');
const namespace = mw.config.get('wgNamespaceNumber');
if (action === 'edit' && namespace === 0) {
console.log('Article edit page detected, checking version and setting up deduplication...');
if (document.readyState === 'complete') {
runDeduplicationWhenReady();
addDeduplicationButton();
} else {
window.addEventListener('load', () => {
runDeduplicationWhenReady();
addDeduplicationButton();
});
}
} else {
console.log('Not on article edit page - action:', action, 'namespace:', namespace);
}
} else {
console.log('MediaWiki not available');
}
// Also add the button when the page is ready (only for article namespace)
setTimeout(() => {
if (typeof mw !== 'undefined') {
const action = mw.config.get('wgAction');
const namespace = mw.config.get('wgNamespaceNumber');
if (action === 'edit' && namespace === 0) {
addDeduplicationButton();
}
}
}, 2000);
// </nowiki>