User:Polygnotus/Scripts/ListGenerator2.js

This is an old revision of this page, as edited by Polygnotus (talk | contribs) at 18:07, 14 June 2025. The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// Wikipedia List Generator - Special:BlankPage/Listgen
// Universal Wikipedia List Copier adapted for dedicated blank page interface

$(document).ready(function() {
    // Only run on Special:BlankPage/Listgen
    if (mw.config.get('wgCanonicalSpecialPageName') === 'Blankpage' && 
        mw.config.get('wgPageName') === 'Special:BlankPage/Listgen') {
        
        // Set up the page
        $('#firstHeading').text('Wikipedia List Generator');
        setupListGeneratorInterface();
    }
});

const CONFIG = {
    API_DELAY: 500,
    MAX_RETRIES: 3,
    BASE_URL: 'https://en.wikipedia.org',
    API_URL: 'https://en.wikipedia.org/w/api.php'
};

// ===== CORE UTILITIES =====

function addTooltip(element, text) {
    element.title = text;
}

function formatItems(items, includeUrls, baseUrl = `${CONFIG.BASE_URL}/wiki/`) {
    if (!includeUrls) return items.join('\n');
    return items.map(item => `${baseUrl}${encodeURIComponent(item.replace(/ /g, '_'))}`).join('\n');
}

async function copyToClipboardOrDownload(text, filename, statusElement) {
    const success = await tryClipboardCopy(text);
    if (!success) {
        statusElement.html(`<p>Clipboard access failed. Click the link below to download items:</p>`);
        offerTextAsDownload(text, filename, statusElement);
    }
    return success;
}

function offerTextAsDownload(text, filename, statusElement) {
    const blob = new Blob([text], {type: 'text/plain'});
    const url = URL.createObjectURL(blob);
    const downloadLink = $('<a>')
        .attr('href', url)
        .attr('download', filename || 'wikipedia-items.txt')
        .text(`Download ${filename || 'items'} as text file`)
        .css('display', 'block')
        .css('margin-top', '10px');
    statusElement.append(downloadLink);
}

async function tryClipboardCopy(text) {
    if (navigator.clipboard?.writeText) {
        try {
            await navigator.clipboard.writeText(text);
            return true;
        } catch {}
    }
    
    // Fallback method
    try {
        const textarea = document.createElement('textarea');
        Object.assign(textarea.style, {
            position: 'fixed',
            left: '-999999px',
            top: '-999999px'
        });
        textarea.value = text;
        document.body.appendChild(textarea);
        textarea.focus();
        textarea.select();
        const success = document.execCommand('copy');
        document.body.removeChild(textarea);
        return success;
    } catch {
        return false;
    }
}

// ===== API UTILITIES =====

async function makeApiRequest(url, retryCount = 0) {
    await new Promise(resolve => setTimeout(resolve, CONFIG.API_DELAY));
    
    try {
        const response = await fetch(url);
        
        if (response.status === 429 || response.status >= 500) {
            if (retryCount < CONFIG.MAX_RETRIES) {
                await new Promise(resolve => setTimeout(resolve, Math.pow(2, retryCount) * 1000));
                return makeApiRequest(url, retryCount + 1);
            }
            throw new Error(`Request failed after ${CONFIG.MAX_RETRIES} retries: ${response.status}`);
        }
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        
        const data = await response.json();
        
        if (data.error?.code === 'maxlag') {
            const waitTime = (data.error.lag || 5 + 2) * 1000;
            await new Promise(resolve => setTimeout(resolve, waitTime));
            return makeApiRequest(url, retryCount);
        }
        
        if (data.error) {
            throw new Error(`API Error: ${data.error.code} - ${data.error.info}`);
        }
        
        return data;
    } catch (error) {
        if (retryCount < CONFIG.MAX_RETRIES) {
            await new Promise(resolve => setTimeout(resolve, 1000));
            return makeApiRequest(url, retryCount + 1);
        }
        throw error;
    }
}

// Generic paginated API fetcher
async function fetchAllPages(apiConfig, statusCallback) {
    let allItems = [];
    let continueToken = null;
    let pagesProcessed = 0;
    
    do {
        const url = apiConfig.buildUrl(continueToken);
        statusCallback(`${apiConfig.progressMessage} (page ${pagesProcessed + 1})...`);
        
        const data = await makeApiRequest(url);
        const { items, continueToken: nextToken } = apiConfig.parseResponse(data);
        
        allItems = allItems.concat(items);
        continueToken = nextToken;
        pagesProcessed++;
        
        statusCallback(`Retrieved ${allItems.length} ${apiConfig.itemType} (page ${pagesProcessed})...`);
    } while (continueToken);
    
    return allItems;
}

// ===== CONSOLIDATED FETCH METHODS =====

async function fetchPaginatedList(listType, params, statusCallback = () => {}) {
    const configs = {
        categoryMembers: {
            list: 'categorymembers',
            titleParam: 'cmtitle',
            continueParam: 'cmcontinue',
            limitParam: 'cmlimit',
            namespaceParam: 'cmnamespace',
            dataPath: 'categorymembers',
            defaultNamespaces: '0|1|2|3|4|5|6|7|8|9|10|11|12|13|15'
        },
        categorySubcategories: {
            list: 'categorymembers',
            titleParam: 'cmtitle',
            continueParam: 'cmcontinue',
            limitParam: 'cmlimit',
            namespaceParam: 'cmnamespace',
            dataPath: 'categorymembers',
            defaultNamespaces: '14'
        },
        backlinks: {
            list: 'backlinks',
            titleParam: 'bltitle',
            continueParam: 'blcontinue',
            limitParam: 'bllimit',
            namespaceParam: 'blnamespace',
            dataPath: 'backlinks'
        },
        prefixPages: {
            list: 'allpages',
            titleParam: 'apprefix',
            continueParam: 'apcontinue',
            limitParam: 'aplimit',
            namespaceParam: 'apnamespace',
            dataPath: 'allpages'
        },
        search: {
            list: 'search',
            titleParam: 'srsearch',
            continueParam: 'sroffset',
            limitParam: 'srlimit',
            dataPath: 'search'
        }
    };
    
    const config = configs[listType];
    if (!config) {
        throw new Error(`Unknown list type: ${listType}`);
    }
    
    return fetchAllPages({
        buildUrl: (continueToken) => {
            let url = `${CONFIG.API_URL}?action=query&list=${config.list}&${config.limitParam}=max&maxlag=5&format=json&origin=*`;
            
            // Add title/search parameter
            if (config.titleParam && params.title) {
                if (listType === 'categoryMembers' || listType === 'categorySubcategories') {
                    url += `&${config.titleParam}=Category:${encodeURIComponent(params.title)}`;
                } else {
                    url += `&${config.titleParam}=${encodeURIComponent(params.title)}`;
                }
            }
            
            // Add namespace parameter
            if (config.namespaceParam) {
                const namespace = params.namespace !== undefined ? params.namespace : config.defaultNamespaces;
                if (namespace !== null) {
                    url += `&${config.namespaceParam}=${namespace}`;
                }
            }
            
            // Add continuation token
            if (continueToken) {
                url += `&${config.continueParam}=${continueToken}`;
            }
            
            return url;
        },
        parseResponse: (data) => ({
            items: data.query?.[config.dataPath]?.map(item => item.title) || [],
            continueToken: data.continue?.[config.continueParam] || null
        }),
        progressMessage: params.progressMessage || `Fetching ${listType}`,
        itemType: params.itemType || 'items'
    }, statusCallback);
}

// Individual fetch methods
async function fetchCategoryMembers(categoryTitle, statusCallback) {
    return fetchPaginatedList('categoryMembers', {
        title: categoryTitle,
        progressMessage: `Fetching items for: ${categoryTitle}`,
        itemType: 'items'
    }, statusCallback);
}

async function fetchCategorySubcategories(categoryTitle, statusCallback) {
    return fetchPaginatedList('categorySubcategories', {
        title: categoryTitle,
        progressMessage: `Fetching subcategories for: ${categoryTitle}`,
        itemType: 'subcategories'
    }, statusCallback);
}

async function fetchBacklinks(targetTitle, namespaces, statusCallback) {
    return fetchPaginatedList('backlinks', {
        title: targetTitle,
        namespace: namespaces,
        progressMessage: `Fetching backlinks for: ${targetTitle}`,
        itemType: 'backlinks'
    }, statusCallback);
}

async function fetchPrefixPages(prefix, namespace, statusCallback) {
    return fetchPaginatedList('prefixPages', {
        title: prefix,
        namespace: namespace,
        progressMessage: `Fetching pages with prefix "${prefix}" in namespace ${namespace}`,
        itemType: 'pages'
    }, statusCallback);
}

async function fetchSearchResults(query, statusCallback) {
    return fetchPaginatedList('search', {
        title: query,
        progressMessage: `Searching for: "${query}"`,
        itemType: 'search results'
    }, statusCallback);
}

// Recursive methods
async function fetchCategoryMembersRecursive(categoryTitle, statusCallback) {
    const visited = new Set();
    const allItems = [];
    const queue = [categoryTitle];
    let totalCategories = 0;
    
    while (queue.length > 0) {
        const currentCategory = queue.shift();
        const categoryKey = `Category:${currentCategory}`;
        
        if (visited.has(categoryKey)) continue;
        visited.add(categoryKey);
        totalCategories++;
        
        statusCallback(`Getting items from "${currentCategory}" (processed ${totalCategories} categories, found ${allItems.length} items, queue: ${queue.length})...`);
        
        const currentItems = await fetchCategoryMembers(currentCategory, statusCallback);
        allItems.push(...currentItems);
        
        const subcategories = await fetchCategorySubcategories(currentCategory, statusCallback);
        for (const subcategory of subcategories) {
            if (!visited.has(subcategory)) {
                queue.push(subcategory.replace('Category:', ''));
            }
        }
    }
    
    return [...new Set(allItems)];
}

// ===== UI SETUP =====

function setupListGeneratorInterface() {
    const content = $('#mw-content-text');
    content.html(`
        <div id="listgen-container" style="max-width: 1200px; margin: 0 auto; padding: 20px;">
            <div style="margin-bottom: 30px;">
                <h2>Wikipedia List Generator</h2>
                <p>Generate lists from Wikipedia categories, search results, backlinks, and more.</p>
            </div>
            
            <div id="listgen-tabs" style="margin-bottom: 20px;">
                <button class="listgen-tab active" data-tab="category">Category Tools</button>
                <button class="listgen-tab" data-tab="backlinks">Whatlinkshere</button>
                <button class="listgen-tab" data-tab="prefix">Prefix Search</button>
                <button class="listgen-tab" data-tab="search">Search Results</button>
            </div>
            
            <div id="category-tab" class="listgen-tab-content active">
                <div class="listgen-section">
                    <h3>Category Tools</h3>
                    <div class="input-group">
                        <label for="category-input">Category name (without "Category:" prefix):</label>
                        <input type="text" id="category-input" placeholder="e.g., American novelists" style="width: 300px; padding: 5px;">
                    </div>
                    <div class="button-group">
                        <button id="cat-members">Get Category Members</button>
                        <button id="cat-members-recursive">Get Members (Recursive)</button>
                        <button id="cat-subcats">Get Subcategories</button>
                        <button id="cat-subcats-recursive">Get Subcategories (Recursive)</button>
                        <button id="cat-both">Get Both</button>
                        <button id="cat-both-recursive">Get Both (Recursive)</button>
                    </div>
                </div>
            </div>
            
            <div id="backlinks-tab" class="listgen-tab-content">
                <div class="listgen-section">
                    <h3>Backlinks Tools</h3>
                    <div class="input-group">
                        <label for="backlinks-input">Page title:</label>
                        <input type="text" id="backlinks-input" placeholder="e.g., United States" style="width: 300px; padding: 5px;">
                    </div>
                    <div class="button-group">
                        <button id="backlinks-all">Get All Backlinks</button>
                        <button id="backlinks-mainspace">Get Mainspace Backlinks</button>
                        <button id="backlinks-non-mainspace">Get Non-Mainspace Backlinks</button>
                    </div>
                </div>
            </div>
            
            <div id="prefix-tab" class="listgen-tab-content">
                <div class="listgen-section">
                    <h3>Prefix Search Tools</h3>
                    <div class="input-group">
                        <label for="prefix-input">Prefix:</label>
                        <input type="text" id="prefix-input" placeholder="e.g., List of" style="width: 300px; padding: 5px;">
                    </div>
                    <div class="input-group">
                        <label for="namespace-select">Namespace:</label>
                        <select id="namespace-select" style="padding: 5px;">
                            <option value="0">Main (0)</option>
                            <option value="1">Talk (1)</option>
                            <option value="2">User (2)</option>
                            <option value="4">Wikipedia (4)</option>
                            <option value="6">File (6)</option>
                            <option value="10">Template (10)</option>
                            <option value="14">Category (14)</option>
                        </select>
                    </div>
                    <div class="button-group">
                        <button id="prefix-search">Get Pages with Prefix</button>
                    </div>
                </div>
            </div>
            
            <div id="search-tab" class="listgen-tab-content">
                <div class="listgen-section">
                    <h3>Search Results Tools</h3>
                    <div class="input-group">
                        <label for="search-input">Search query:</label>
                        <input type="text" id="search-input" placeholder="e.g., American authors" style="width: 300px; padding: 5px;">
                    </div>
                    <div class="button-group">
                        <button id="search-results">Get Search Results</button>
                    </div>
                </div>
            </div>
            
            <div class="options-section" style="margin: 20px 0; padding: 15px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 3px;">
                <label>
                    <input type="checkbox" id="include-urls"> Include URLs
                </label>
                <span style="margin-left: 10px; color: #666; font-size: 12px;">Check to include full Wikipedia URLs for each item</span>
            </div>
            
            <div id="status-section" style="margin: 20px 0; padding: 15px; background: #f0f0f0; border-radius: 3px; min-height: 50px;">
                <div id="status-text">Ready to generate lists...</div>
            </div>
        </div>
    `);
    
    // Add CSS styles
    $('<style>').text(`
        .listgen-tab {
            background: #f8f9fa;
            border: 1px solid #a2a9b1;
            border-bottom: none;
            padding: 10px 15px;
            cursor: pointer;
            margin-right: 2px;
        }
        .listgen-tab.active {
            background: white;
            font-weight: bold;
        }
        .listgen-tab-content {
            display: none;
            background: white;
            border: 1px solid #a2a9b1;
            padding: 20px;
            border-radius: 0 0 3px 3px;
        }
        .listgen-tab-content.active {
            display: block;
        }
        .listgen-section h3 {
            margin-top: 0;
            margin-bottom: 15px;
        }
        .input-group {
            margin-bottom: 15px;
        }
        .input-group label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }
        .button-group button {
            margin-right: 10px;
            margin-bottom: 10px;
            padding: 8px 12px;
            cursor: pointer;
            background: #0645ad;
            color: white;
            border: none;
            border-radius: 3px;
        }
        .button-group button:hover {
            background: #0b57d0;
        }
        .button-group button:disabled {
            background: #ccc;
            cursor: not-allowed;
        }
    `).appendTo('head');
    
    setupEventHandlers();
}

function setupEventHandlers() {
    // Tab switching
    $('.listgen-tab').on('click', function() {
        const tabName = $(this).data('tab');
        $('.listgen-tab').removeClass('active');
        $('.listgen-tab-content').removeClass('active');
        $(this).addClass('active');
        $(`#${tabName}-tab`).addClass('active');
    });
    
    // Button handlers
    $('#cat-members').on('click', () => handleCategoryAction('members'));
    $('#cat-members-recursive').on('click', () => handleCategoryAction('members-recursive'));
    $('#cat-subcats').on('click', () => handleCategoryAction('subcats'));
    $('#cat-subcats-recursive').on('click', () => handleCategoryAction('subcats-recursive'));
    $('#cat-both').on('click', () => handleCategoryAction('both'));
    $('#cat-both-recursive').on('click', () => handleCategoryAction('both-recursive'));
    
    $('#backlinks-all').on('click', () => handleBacklinksAction('all'));
    $('#backlinks-mainspace').on('click', () => handleBacklinksAction('mainspace'));
    $('#backlinks-non-mainspace').on('click', () => handleBacklinksAction('non-mainspace'));
    
    $('#prefix-search').on('click', handlePrefixAction);
    $('#search-results').on('click', handleSearchAction);
}

// ===== ACTION HANDLERS =====

async function handleCategoryAction(action) {
    const categoryName = $('#category-input').val().trim();
    if (!categoryName) {
        updateStatus('Please enter a category name.');
        return;
    }
    
    const includeUrls = $('#include-urls').is(':checked');
    const statusCallback = (msg) => updateStatus(msg);
    
    try {
        let items = [];
        let filename = '';
        
        switch (action) {
            case 'members':
                items = await fetchCategoryMembers(categoryName, statusCallback);
                filename = `${categoryName}_members`;
                break;
            case 'members-recursive':
                items = await fetchCategoryMembersRecursive(categoryName, statusCallback);
                filename = `${categoryName}_members_recursive`;
                break;
            case 'subcats':
                items = await fetchCategorySubcategories(categoryName, statusCallback);
                filename = `${categoryName}_subcategories`;
                break;
            case 'subcats-recursive':
                items = await fetchCategorySubcategoriesRecursive(categoryName, statusCallback);
                filename = `${categoryName}_subcategories_recursive`;
                break;
            case 'both':
                const [members, subcats] = await Promise.all([
                    fetchCategoryMembers(categoryName, statusCallback),
                    fetchCategorySubcategories(categoryName, statusCallback)
                ]);
                items = [...members, ...subcats];
                filename = `${categoryName}_both`;
                break;
            case 'both-recursive':
                items = await fetchCategoryBothRecursive(categoryName, statusCallback);
                filename = `${categoryName}_both_recursive`;
                break;
        }
        
        if (items.length === 0) {
            updateStatus('No items found.');
            return;
        }
        
        const formattedText = formatItems(items, includeUrls);
        const copySuccess = await copyToClipboardOrDownload(formattedText, filename, $('#status-section'));
        
        if (copySuccess) {
            updateStatus(`Successfully copied ${items.length} items to clipboard.`);
        }
    } catch (error) {
        updateStatus(`Error: ${error.message}`);
    }
}

async function handleBacklinksAction(type) {
    const targetTitle = $('#backlinks-input').val().trim();
    if (!targetTitle) {
        updateStatus('Please enter a page title.');
        return;
    }
    
    const includeUrls = $('#include-urls').is(':checked');
    const statusCallback = (msg) => updateStatus(msg);
    
    try {
        let items = [];
        let filename = '';
        
        switch (type) {
            case 'all':
                items = await fetchBacklinks(targetTitle, null, statusCallback);
                filename = 'all_backlinks';
                break;
            case 'mainspace':
                items = await fetchBacklinks(targetTitle, '0', statusCallback);
                filename = 'mainspace_backlinks';
                break;
            case 'non-mainspace':
                const allBacklinks = await fetchBacklinks(targetTitle, null, statusCallback);
                updateStatus('Filtering out mainspace backlinks...');
                const mainspaceBacklinks = await fetchBacklinks(targetTitle, '0', statusCallback);
                const mainspaceSet = new Set(mainspaceBacklinks);
                items = allBacklinks.filter(link => !mainspaceSet.has(link));
                filename = 'non_mainspace_backlinks';
                break;
        }
        
        if (items.length === 0) {
            updateStatus('No backlinks found.');
            return;
        }
        
        const formattedText = formatItems(items, includeUrls);
        const copySuccess = await copyToClipboardOrDownload(formattedText, filename, $('#status-section'));
        
        if (copySuccess) {
            updateStatus(`Successfully copied ${items.length} backlinks to clipboard.`);
        }
    } catch (error) {
        updateStatus(`Error: ${error.message}`);
    }
}

async function handlePrefixAction() {
    const prefix = $('#prefix-input').val().trim();
    const namespace = $('#namespace-select').val();
    
    if (!prefix) {
        updateStatus('Please enter a prefix.');
        return;
    }
    
    const includeUrls = $('#include-urls').is(':checked');
    const statusCallback = (msg) => updateStatus(msg);
    
    try {
        const items = await fetchPrefixPages(prefix, namespace, statusCallback);
        
        if (items.length === 0) {
            updateStatus(`No pages found with prefix "${prefix}" in namespace ${namespace}.`);
            return;
        }
        
        const filename = `prefix_${prefix.replace(/[^a-zA-Z0-9]/g, '_')}`;
        const formattedText = formatItems(items, includeUrls);
        const copySuccess = await copyToClipboardOrDownload(formattedText, filename, $('#status-section'));
        
        if (copySuccess) {
            updateStatus(`Successfully copied ${items.length} pages to clipboard.`);
        }
    } catch (error) {
        updateStatus(`Error: ${error.message}`);
    }
}

async function handleSearchAction() {
    const query = $('#search-input').val().trim();
    if (!query) {
        updateStatus('Please enter a search query.');
        return;
    }
    
    const includeUrls = $('#include-urls').is(':checked');
    const statusCallback = (msg) => updateStatus(msg);
    
    try {
        const items = await fetchSearchResults(query, statusCallback);
        
        if (items.length === 0) {
            updateStatus(`No search results found for "${query}".`);
            return;
        }
        
        const filename = `search_${query.replace(/[^a-zA-Z0-9]/g, '_')}`;
        const formattedText = formatItems(items, includeUrls);
        const copySuccess = await copyToClipboardOrDownload(formattedText, filename, $('#status-section'));
        
        if (copySuccess) {
            updateStatus(`Successfully copied ${items.length} search results to clipboard.`);
        }
    } catch (error) {
        updateStatus(`Error: ${error.message}`);
    }
}

// Recursive category methods
async function fetchCategorySubcategoriesRecursive(categoryTitle, statusCallback) {
    const visited = new Set();
    const allSubcategories = [];
    const queue = [`Category:${categoryTitle}`];
    
    while (queue.length > 0) {
        const currentCategory = queue.shift();
        
        if (visited.has(currentCategory)) continue;
        visited.add(currentCategory);
        
        statusCallback(`Exploring subcategories (found ${allSubcategories.length} categories, queue: ${queue.length})...`);
        
        const categoryNameForApi = currentCategory.replace('Category:', '');
        const directSubcategories = await fetchCategorySubcategories(categoryNameForApi, statusCallback);
        
        for (const subcategory of directSubcategories) {
            if (!visited.has(subcategory)) {
                allSubcategories.push(subcategory);
                queue.push(subcategory);
            }
        }
    }
    
    return [...new Set(allSubcategories)];
}

async function fetchCategoryBothRecursive(categoryTitle, statusCallback) {
    const visited = new Set();
    const allItems = [];
    const allSubcategories = [];
    const queue = [categoryTitle];
    let totalCategories = 0;
    
    while (queue.length > 0) {
        const currentCategory = queue.shift();
        const categoryKey = `Category:${currentCategory}`;
        
        if (visited.has(categoryKey)) continue;
        visited.add(categoryKey);
        totalCategories++;
        
        statusCallback(`Getting items and subcategories from "${currentCategory}" (processed ${totalCategories} categories, found ${allItems.length} items, ${allSubcategories.length} subcategories, queue: ${queue.length})...`);
        
        const [currentItems, directSubcategories] = await Promise.all([
            fetchCategoryMembers(currentCategory, statusCallback),
            fetchCategorySubcategories(currentCategory, statusCallback)
        ]);
        
        allItems.push(...currentItems);
        
        for (const subcategory of directSubcategories) {
            if (!visited.has(subcategory)) {
                allSubcategories.push(subcategory);
                queue.push(subcategory.replace('Category:', ''));
            }
        }
    }
    
    return [...new Set([...allItems, ...allSubcategories])];
}

function updateStatus(message) {
    $('#status-text').html(message);
}

console.log('Wikipedia List Generator loaded successfully!');