//needed for testing DeduplicateReferences.js
// Wikipedia Duplicate Reference Finder for common.js
// Adds a tool to search for articles with exact duplicate references
$(document).ready(function() {
// Only run on Wikipedia
if (mw.config.get('wgSiteName') !== 'Wikipedia') return;
// Add portlet link to toolbox
mw.util.addPortletLink(
'p-tb',
'#',
'Find Duplicate Refs',
'find-duplicate-refs',
'Search for articles with duplicate references'
);
// Add the search interface
$('#find-duplicate-refs').click(function(e) {
e.preventDefault();
showDuplicateRefFinder();
});
function showDuplicateRefFinder() {
// Remove existing dialog if present
$('#duplicate-ref-finder').remove();
// Create the search interface
const dialog = $(`
<div id="duplicate-ref-finder" style="
position: fixed;
top: 50px;
right: 20px;
width: 400px;
background: white;
border: 2px solid #0645ad;
border-radius: 5px;
padding: 15px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
z-index: 1000;
font-family: sans-serif;
">
<h3 style="margin-top: 0; color: #0645ad;">Duplicate Reference Finder</h3>
<div style="margin-bottom: 10px;">
<label>Search Method:</label><br>
<input type="radio" name="searchMethod" value="random" id="method-random" checked>
<label for="method-random">Random articles</label><br>
<input type="radio" name="searchMethod" value="category" id="method-category">
<label for="method-category">From category</label><br>
<input type="radio" name="searchMethod" value="search" id="method-search">
<label for="method-search">Search term</label>
</div>
<div id="search-input" style="margin-bottom: 10px; display: none;">
<input type="text" id="search-term" placeholder="Enter search term or category" style="width: 100%; padding: 5px;">
</div>
<div style="margin-bottom: 10px;">
<label>Min references to check: </label>
<input type="number" id="min-refs" value="10" min="5" max="100" style="width: 60px;">
</div>
<div style="margin-bottom: 15px;">
<button id="start-search" style="background: #0645ad; color: white; padding: 8px 15px; border: none; border-radius: 3px; cursor: pointer;">Start Search</button>
<button id="stop-search" style="background: #d33; color: white; padding: 8px 15px; border: none; border-radius: 3px; cursor: pointer; margin-left: 5px;" disabled>Stop</button>
<button id="close-finder" style="float: right; background: #666; color: white; padding: 8px 15px; border: none; border-radius: 3px; cursor: pointer;">Close</button>
</div>
<div id="search-status" style="margin-bottom: 10px; font-weight: bold;"></div>
<div id="search-progress" style="margin-bottom: 10px;"></div>
<div id="results" style="max-height: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; background: #f9f9f9;"></div>
</div>
`);
$('body').append(dialog);
// Event handlers
$('input[name="searchMethod"]').change(function() {
if ($(this).val() === 'random') {
$('#search-input').hide();
} else {
$('#search-input').show();
$('#search-term').attr('placeholder',
$(this).val() === 'category' ? 'Enter category name (e.g., "Living people")' : 'Enter search term'
);
}
});
$('#close-finder').click(() => $('#duplicate-ref-finder').remove());
let searchActive = false;
let searchAborted = false;
// Make searchAborted accessible to the performSearch function
window.currentSearchAborted = false;
$('#start-search').click(async function() {
if (searchActive) return;
searchActive = true;
window.currentSearchAborted = false;
$(this).prop('disabled', true);
$('#stop-search').prop('disabled', false);
$('#results').empty();
$('#search-status').text('Starting search...');
try {
await performSearch();
} catch (error) {
$('#search-status').text('Search error: ' + error.message);
}
searchActive = false;
$('#start-search').prop('disabled', false);
$('#stop-search').prop('disabled', true);
});
$('#stop-search').click(function() {
window.currentSearchAborted = true;
$('#search-status').text('Search stopped by user.');
});
}
async function performSearch() {
const method = $('input[name="searchMethod"]:checked').val();
const minRefs = parseInt($('#min-refs').val()) || 10;
const searchTerm = $('#search-term').val();
let articles = [];
// Get list of articles to check
if (method === 'random') {
articles = await getRandomArticles(50);
} else if (method === 'category' && searchTerm) {
articles = await getCategoryArticles(searchTerm, 50);
} else if (method === 'search' && searchTerm) {
articles = await getSearchResults(searchTerm, 50);
} else {
$('#search-status').text('Please enter a search term or category.');
return;
}
if (articles.length === 0) {
$('#search-status').text('No articles found to check.');
return;
}
$('#search-status').text(`Checking ${articles.length} articles...`);
let checkedCount = 0;
let foundCount = 0;
for (const article of articles) {
if (window.currentSearchAborted) break;
checkedCount++;
$('#search-progress').text(`Checked: ${checkedCount}/${articles.length} | Found: ${foundCount}`);
try {
const duplicateCount = await checkArticleForDuplicates(article.title, minRefs);
if (duplicateCount > 0) {
foundCount++;
addResult(article.title, duplicateCount);
}
} catch (error) {
console.log(`Error checking ${article.title}:`, error);
}
// Small delay to avoid overwhelming the API
await new Promise(resolve => setTimeout(resolve, 200));
}
$('#search-status').text(`Search complete! Checked ${checkedCount} articles, found ${foundCount} with duplicates.`);
}
async function getRandomArticles(count) {
const api = new mw.Api();
const result = await api.get({
action: 'query',
list: 'random',
rnnamespace: 0, // Main namespace only
rnlimit: count,
format: 'json'
});
return result.query.random;
}
async function getCategoryArticles(category, count) {
const api = new mw.Api();
// Remove "Category:" prefix if present
category = category.replace(/^Category:/, '');
const result = await api.get({
action: 'query',
list: 'categorymembers',
cmtitle: 'Category:' + category,
cmnamespace: 0, // Main namespace only
cmlimit: count,
format: 'json'
});
return result.query.categorymembers;
}
async function getSearchResults(term, count) {
const api = new mw.Api();
const result = await api.get({
action: 'query',
list: 'search',
srsearch: term,
srnamespace: 0, // Main namespace only
srlimit: count,
format: 'json'
});
return result.query.search;
}
async function checkArticleForDuplicates(title, minRefs) {
const api = new mw.Api();
// Get the article content
const result = await api.get({
action: 'query',
titles: title,
prop: 'revisions',
rvprop: 'content',
rvslots: 'main',
format: 'json'
});
const pages = result.query.pages;
const pageId = Object.keys(pages)[0];
const page = pages[pageId];
if (!page.revisions || !page.revisions[0]) {
return 0;
}
const content = page.revisions[0].slots.main['*'];
// Extract all <ref> tags
const refRegex = /<ref[^>]*>[\s\S]*?<\/ref>/gi;
const refs = content.match(refRegex) || [];
if (refs.length < minRefs) {
return 0;
}
// Count duplicates
const refCounts = {};
let duplicateCount = 0;
refs.forEach(ref => {
// Normalize whitespace for comparison
const normalizedRef = ref.replace(/\s+/g, ' ').trim();
if (refCounts[normalizedRef]) {
refCounts[normalizedRef]++;
if (refCounts[normalizedRef] === 2) {
duplicateCount++; // First time we see a duplicate
}
} else {
refCounts[normalizedRef] = 1;
}
});
return duplicateCount;
}
function addResult(title, duplicateCount) {
const editUrl = `/w/index.php?title=${encodeURIComponent(title)}&action=edit`;
const viewUrl = `/wiki/${encodeURIComponent(title)}`;
const resultHtml = `
<div style="margin-bottom: 10px; padding: 8px; border: 1px solid #ddd; border-radius: 3px; background: white;">
<strong><a href="${viewUrl}" target="_blank">${mw.html.escape(title)}</a></strong><br>
<span style="color: #d33;">${duplicateCount} duplicate reference${duplicateCount > 1 ? 's' : ''}</span><br>
<a href="${editUrl}" target="_blank" style="color: #0645ad;">Edit article</a>
</div>
`;
$('#results').append(resultHtml);
// Auto-scroll to bottom
const resultsDiv = $('#results')[0];
resultsDiv.scrollTop = resultsDiv.scrollHeight;
}
});