// Wikipedia Category Items Copier
// This script adds three buttons to Wikipedia category pages:
// 1. "Copy Items" - Copies all items in the current category via API
// 2. "Copy All Items" - Copies all items in the current category and its subcategories via API
// 3. "Copy Subcategories" - Copies all subcategories recursively (no items)
// Only run on Wikipedia category pages
if (window.___location.href.includes('/wiki/Category:')) {
// Extract the category name from the URL
const categoryName = decodeURIComponent(window.___location.pathname.split('/Category:')[1]);
console.log("Category Name:", categoryName);
// Create a container for our buttons
const container = document.createElement('div');
container.style.padding = '10px';
container.style.margin = '10px 0';
container.style.backgroundColor = '#f8f9fa';
container.style.border = '1px solid #a2a9b1';
container.style.borderRadius = '3px';
// Create the "Copy Items" button
const copyItemsBtn = document.createElement('button');
copyItemsBtn.textContent = 'Copy Items from this Category';
copyItemsBtn.style.marginRight = '10px';
copyItemsBtn.style.padding = '8px 12px';
copyItemsBtn.style.cursor = 'pointer';
// Create the "Copy All Items" button
const copyAllItemsBtn = document.createElement('button');
copyAllItemsBtn.textContent = 'Copy Items from All Subcategories';
copyAllItemsBtn.style.marginRight = '10px';
copyAllItemsBtn.style.padding = '8px 12px';
copyAllItemsBtn.style.cursor = 'pointer';
// Create the "Copy Subcategories" button
const copySubcatsBtn = document.createElement('button');
copySubcatsBtn.textContent = 'Copy All Subcategories';
copySubcatsBtn.style.padding = '8px 12px';
copySubcatsBtn.style.cursor = 'pointer';
// Create status text
const statusText = document.createElement('div');
statusText.style.marginTop = '10px';
statusText.style.color = '#555';
// Add buttons to container
container.appendChild(copyItemsBtn);
container.appendChild(copyAllItemsBtn);
container.appendChild(copySubcatsBtn);
container.appendChild(statusText);
// Insert container after the page title
const pageTitleHeading = document.querySelector('.mw-first-heading');
if (pageTitleHeading) {
pageTitleHeading.parentNode.insertBefore(container, pageTitleHeading.nextSibling);
} else {
document.querySelector('#content').prepend(container);
}
// Function that creates a download link as an alternative to clipboard
function offerTextAsDownload(text, filename) {
// Create blob from text
const blob = new Blob([text], {type: 'text/plain'});
const url = URL.createObjectURL(blob);
// Create download link
const downloadLink = document.createElement('a');
downloadLink.href = url;
downloadLink.download = filename || 'wikipedia-category-items.txt';
downloadLink.textContent = `Download ${filename || 'items'} as text file`;
downloadLink.style.display = 'block';
downloadLink.style.marginTop = '10px';
// Add to status container
statusText.appendChild(downloadLink);
return true;
}
// Function to copy text to clipboard or offer download if copying fails
function copyToClipboardOrDownload(text, categoryName) {
return new Promise((resolve) => {
// Try to copy to clipboard first
tryClipboardCopy(text).then(success => {
if (success) {
resolve(true);
} else {
// If clipboard fails, offer download instead
const filename = `${categoryName.replace(/[^a-z0-9]/gi, '_')}-items.txt`;
offerTextAsDownload(text, filename);
statusText.innerHTML = `<p>Clipboard access failed. Click the link below to download items:</p>` + statusText.innerHTML;
resolve(false);
}
});
});
}
// Try multiple clipboard methods
function tryClipboardCopy(text) {
return new Promise((resolve) => {
// First try the modern Clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text)
.then(() => resolve(true))
.catch(() => {
// If Clipboard API fails, try execCommand
try {
const textarea = document.createElement('textarea');
textarea.value = text;
// Position off-screen but available
textarea.style.position = 'fixed';
textarea.style.left = '-999999px';
textarea.style.top = '-999999px';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
const success = document.execCommand('copy');
document.body.removeChild(textarea);
if (success) {
resolve(true);
} else {
resolve(false);
}
} catch (e) {
console.error("Clipboard operations failed:", e);
resolve(false);
}
});
} else {
// No clipboard API, try execCommand directly
try {
const textarea = document.createElement('textarea');
textarea.value = text;
// Position off-screen but available
textarea.style.position = 'fixed';
textarea.style.left = '-999999px';
textarea.style.top = '-999999px';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
const success = document.execCommand('copy');
document.body.removeChild(textarea);
if (success) {
resolve(true);
} else {
resolve(false);
}
} catch (e) {
console.error("Clipboard operations failed:", e);
resolve(false);
}
}
});
}
// Function to get all members of a category using Wikipedia API
async function getCategoryMembers(categoryTitle, continueToken = null) {
try {
// Base API URL
let apiUrl = `https://en.wikipedia.org/w/api.php?action=query&list=categorymembers&cmtitle=Category:${encodeURIComponent(categoryTitle)}&cmlimit=500&format=json&origin=*`;
// Add continue token if provided
if (continueToken) {
apiUrl += `&cmcontinue=${continueToken}`;
}
statusText.textContent = `Fetching category members for: ${categoryTitle}...`;
// Add a small delay to avoid hammering the server
await new Promise(resolve => setTimeout(resolve, 800));
const response = await fetch(apiUrl);
const data = await response.json();
if (!data.query || !data.query.categorymembers) {
console.error("Unexpected API response:", data);
return { members: [], continueToken: null };
}
// Extract members and continue token
const members = data.query.categorymembers.map(member => member.title);
const nextContinueToken = data.continue ? data.continue.cmcontinue : null;
return { members, continueToken: nextContinueToken };
} catch (error) {
console.error("API request error:", error);
statusText.textContent = `Error fetching category members: ${error.message}`;
return { members: [], continueToken: null };
}
}
// Function to get all subcategories of a category
async function getSubcategories(categoryTitle, continueToken = null) {
try {
// Base API URL for subcategories (only get items with namespace 14, which is Category)
let apiUrl = `https://en.wikipedia.org/w/api.php?action=query&list=categorymembers&cmtitle=Category:${encodeURIComponent(categoryTitle)}&cmnamespace=14&cmlimit=500&format=json&origin=*`;
// Add continue token if provided
if (continueToken) {
apiUrl += `&cmcontinue=${continueToken}`;
}
statusText.textContent = `Fetching subcategories for: ${categoryTitle}...`;
// Add a small delay to avoid hammering the server
await new Promise(resolve => setTimeout(resolve, 300));
const response = await fetch(apiUrl);
const data = await response.json();
if (!data.query || !data.query.categorymembers) {
console.error("Unexpected API response:", data);
return { subcategories: [], continueToken: null };
}
// Extract subcategories and continue token
const subcategories = data.query.categorymembers.map(member => member.title.replace('Category:', ''));
const nextContinueToken = data.continue ? data.continue.cmcontinue : null;
return { subcategories, continueToken: nextContinueToken };
} catch (error) {
console.error("API request error:", error);
statusText.textContent = `Error fetching subcategories: ${error.message}`;
return { subcategories: [], continueToken: null };
}
}
// Function to get all non-category members of a category
async function getNonCategoryMembers(categoryTitle, continueToken = null) {
try {
// Base API URL for non-category members (exclude namespace 14, which is Category)
let apiUrl = `https://en.wikipedia.org/w/api.php?action=query&list=categorymembers&cmtitle=Category:${encodeURIComponent(categoryTitle)}&cmnamespace=0|1|2|3|4|5|6|7|8|9|10|11|12|13|15&cmlimit=500&format=json&origin=*`;
// Add continue token if provided
if (continueToken) {
apiUrl += `&cmcontinue=${continueToken}`;
}
statusText.textContent = `Fetching items for: ${categoryTitle}...`;
// Add a small delay to avoid hammering the server
await new Promise(resolve => setTimeout(resolve, 300));
const response = await fetch(apiUrl);
const data = await response.json();
if (!data.query || !data.query.categorymembers) {
console.error("Unexpected API response:", data);
return { members: [], continueToken: null };
}
// Extract members and continue token
const members = data.query.categorymembers.map(member => member.title);
const nextContinueToken = data.continue ? data.continue.cmcontinue : null;
return { members, continueToken: nextContinueToken };
} catch (error) {
console.error("API request error:", error);
statusText.textContent = `Error fetching items: ${error.message}`;
return { members: [], continueToken: null };
}
}
// Function to get all members of a category, handling pagination
async function getAllCategoryMembers(categoryTitle) {
let allMembers = [];
let continueToken = null;
let pagesProcessed = 0;
do {
const { members, continueToken: nextToken } = await getNonCategoryMembers(categoryTitle, continueToken);
allMembers = allMembers.concat(members);
continueToken = nextToken;
pagesProcessed++;
statusText.innerHTML = `Retrieved ${allMembers.length} items from "${categoryTitle}" (page ${pagesProcessed})...`;
// Add a longer pause between requests to be gentler on the API
if (continueToken) {
statusText.innerHTML += ` Pausing before next request...`;
await new Promise(resolve => setTimeout(resolve, 1000));
}
} while (continueToken);
return allMembers;
}
// Function to get all subcategories of a category, handling pagination
async function getAllSubcategories(categoryTitle) {
let allSubcategories = [];
let continueToken = null;
let pagesProcessed = 0;
do {
const { subcategories, continueToken: nextToken } = await getSubcategories(categoryTitle, continueToken);
allSubcategories = allSubcategories.concat(subcategories);
continueToken = nextToken;
pagesProcessed++;
// Add a longer pause between requests
if (continueToken) {
statusText.innerHTML += ` Pausing before next subcategory request...`;
await new Promise(resolve => setTimeout(resolve, 1000));
}
} while (continueToken);
return allSubcategories;
}
// Function to recursively get all subcategories
async function getAllSubcategoriesRecursive(categoryTitle, processedCategories = new Set(), depth = 0) {
// Prevent infinite loops and excessive depth
if (processedCategories.has(categoryTitle) || depth > 10) {
return [];
}
processedCategories.add(categoryTitle);
statusText.innerHTML = `Exploring subcategories of "${categoryTitle}" (depth ${depth}, found ${processedCategories.size} categories so far)...`;
// Get direct subcategories
const directSubcategories = await getAllSubcategories(categoryTitle);
let allSubcategories = [...directSubcategories];
// Recursively get subcategories of each subcategory
for (const subcategory of directSubcategories) {
if (!processedCategories.has(subcategory)) {
const nestedSubcategories = await getAllSubcategoriesRecursive(subcategory, processedCategories, depth + 1);
allSubcategories = allSubcategories.concat(nestedSubcategories);
}
}
return allSubcategories;
}
// Handle "Copy Items" button click
copyItemsBtn.addEventListener('click', async () => {
statusText.innerHTML = 'Gathering items from this category via API...';
try {
const items = await getAllCategoryMembers(categoryName);
if (items.length === 0) {
statusText.innerHTML = 'No items found in this category.';
return;
}
const copySuccess = await copyToClipboardOrDownload(items.join('\n'), categoryName);
if (copySuccess) {
statusText.innerHTML = `Successfully copied ${items.length} items to clipboard.`;
}
} catch (error) {
statusText.innerHTML = `Error: ${error.message}`;
console.error('Error:', error);
}
});
// Handle "Copy All Items" button click
copyAllItemsBtn.addEventListener('click', async () => {
statusText.innerHTML = 'Gathering items from all subcategories via API (this may take a while)...';
try {
// Get items from the current category
let allItems = await getAllCategoryMembers(categoryName);
statusText.innerHTML = `Found ${allItems.length} items in main category. Checking subcategories...`;
// Get all subcategories
const subcategories = await getAllSubcategories(categoryName);
statusText.innerHTML = `Found ${subcategories.length} subcategories. Processing...`;
// Set to track processed categories
const processedCategories = new Set(); // To avoid processing the same category twice
// Process each subcategory
for (let i = 0; i < subcategories.length; i++) {
const subcategoryTitle = subcategories[i];
if (!processedCategories.has(subcategoryTitle)) {
processedCategories.add(subcategoryTitle);
const subcategoryItems = await getAllCategoryMembers(subcategoryTitle);
allItems = allItems.concat(subcategoryItems);
statusText.innerHTML = `Processed ${i + 1}/${subcategories.length} subcategories. Found ${allItems.length} items so far...`;
}
}
// Deduplicate items
const uniqueItems = [...new Set(allItems)];
if (uniqueItems.length === 0) {
statusText.innerHTML = 'No items found in this category or its subcategories.';
return;
}
const copySuccess = await copyToClipboardOrDownload(uniqueItems.join('\n'), categoryName + '_all');
if (copySuccess) {
statusText.innerHTML = `Successfully copied ${uniqueItems.length} unique items to clipboard.`;
}
} catch (error) {
statusText.innerHTML = `Error: ${error.message}`;
console.error('Error:', error);
}
});
// Handle "Copy Subcategories" button click
copySubcatsBtn.addEventListener('click', async () => {
statusText.innerHTML = 'Gathering all subcategories recursively via API (this may take a while)...';
try {
const processedCategories = new Set();
const allSubcategories = await getAllSubcategoriesRecursive(categoryName, processedCategories);
// Deduplicate subcategories
const uniqueSubcategories = [...new Set(allSubcategories)];
if (uniqueSubcategories.length === 0) {
statusText.innerHTML = 'No subcategories found.';
return;
}
const copySuccess = await copyToClipboardOrDownload(uniqueSubcategories.join('\n'), categoryName + '_subcategories');
if (copySuccess) {
statusText.innerHTML = `Successfully copied ${uniqueSubcategories.length} unique subcategories to clipboard.`;
}
} catch (error) {
statusText.innerHTML = `Error: ${error.message}`;
console.error('Error:', error);
}
});
console.log('Wikipedia Category Copier script (API version) has been loaded successfully!');
} else {
// Do nothing on non-category pages
console.log('Wikipedia Category Copier: Not a category page, script inactive.');
}