HTML Table to Markdown/XWiki/CSV Converter

Convert HTML tables to Markdown/XWiki/CSV format

目前為 2023-10-02 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         HTML Table to Markdown/XWiki/CSV Converter
// @namespace    tungxd301
// @version      1.0
// @description  Convert HTML tables to Markdown/XWiki/CSV format
// @author       Tung Dinh
// @match        *://*/*
// @run-at       document-end
// @grant        GM_setClipboard
// @license      Tung Dinh
// ==/UserScript==


(function () {
    'use strict';

    // Configuration options
    const config = {
        waitForDOMTimeout: 1000,
        toastDuration: 3000, // Duration of toast message display (in milliseconds)
        maxAllowedTotalPaginatedPages: 100,
        childList: true, // Watch for changes to the child nodes (elements added or removed)
        subtree: true,   // Watch for changes in the whole subtree, not just the immediate children
    };

    const a = document.querySelector('[href*="&page="]');

    const loadingContainer = document.createElement('div');
    loadingContainer.id = 'loading-container';

    // Create the loading circle element
    const loadingCircle = document.createElement('div');
    loadingCircle.id = 'loading-circle';
    loadingContainer.appendChild(loadingCircle);

    // Create the loading progress element
    const loadingProgress = document.createElement('div');
    loadingProgress.id = 'loading-progress';
    loadingContainer.appendChild(loadingProgress);

    // Function to convert an HTML table to Markdown
    function tableToMarkdown(table) {
        let markdown = '|';

        // Iterate through table headers
        table.querySelectorAll('th').forEach(header => {
            markdown += header.textContent.trim() + '|';
        });

        markdown += '\n|';

        // Add line separator below headers
        table.querySelectorAll('th').forEach(() => {
            markdown += ' --- |';
        });

        // Iterate through table rows
        table.querySelectorAll('tr').forEach(row => {
            row.querySelectorAll('td').forEach(cell => {
                markdown += cell.textContent.trim() + '|';
            });
            markdown += '\n|';
        });

        return markdown.slice(0, -1);
    }

    // Function to convert an HTML table to XWiki syntax
    function tableToXWiki(table) {
        let xwikiSyntax = '';

        // Iterate through table rows
        table.querySelectorAll('tr').forEach(row => {
            row.querySelectorAll('th, td').forEach(cell => {
                // Determine cell type (header or data)
                const cellType = cell.tagName === 'TH' ? 'th' : 'td';

                // Append cell content to XWiki syntax with proper formatting
                xwikiSyntax += `|${cell.textContent.trim()}`;
                if (cellType === 'th') {
                    xwikiSyntax += ' (header)';
                }
            });

            // Add a new row
            xwikiSyntax += '\n';
        });

        return xwikiSyntax.trim();
    }

    // Function to convert an HTML table to CSV format
    function tableToCSV(table) {
        const rows = table.querySelectorAll('tr');
        let csv = '';

        for (let i = 0; i < rows.length; i++) {
            const row = rows[i].querySelectorAll('th, td');
            for (let j = 0; j < row.length; j++) {
                csv += '"' + row[j].textContent.trim() + '"';
                if (j < row.length - 1) {
                    csv += ',';
                }
            }
            csv += '\n';
        }

        return csv;
    }

    // Function to initiate the download
    function downloadCSV(table, tableIndex) {
        const csvContent = tableToCSV(table);
        const blob = new Blob([csvContent], {type: 'text/csv'});
        const url = URL.createObjectURL(blob);

        const link = document.createElement('a');
        link.href = url;
        link.download = `table_${tableIndex}.csv`;
        link.click();

        URL.revokeObjectURL(url);
    }

    async function downloadCSVAllPages(tableIndex) {
        showToast("Downloading CSV...Hang tight!");

        let page = 1;
        let mergedTable = document.createElement('table');
        let currentTables = await fetchTableAllPages(page);
        while (page < config.maxAllowedTotalPaginatedPages
        && currentTables != null
        && currentTables.length > 0
        && currentTables.length <= tableIndex + 1) {
            updateLoadingProgress(page);

            let table = currentTables[tableIndex];
            // Loop through each row in the table
            var rows = table.querySelectorAll('tr');
            rows.forEach(function (row, rowIndex) {
                // Clone the row from the original table
                var clonedRow = row.cloneNode(true);

                // If it's not the first table, and it's the first row (header row), skip it
                if (page > 1 && rowIndex === 0) {
                    return;
                }

                // Append the cloned row to the merged table
                mergedTable.appendChild(clonedRow);
            });
            currentTables = await fetchTableAllPages(++page);
        }
        downloadCSV(mergedTable, tableIndex);

        loadingContainer.remove();
    }

    async function fetchTableAllPages(page) {
        let splitPage = a.href.split('page=') || '';
        let pagePart = splitPage[1];
        let basePage = getFirstDigits(pagePart);
        let targetPagePart = pagePart.replace(basePage, page);
        let url = splitPage[0] + 'page=' + targetPagePart;
        return await fetch(url)
            .then(response => response.text())
            .then(html => {
                const parser = new DOMParser();
                const doc = parser.parseFromString(html, 'text/html');
                return doc.querySelectorAll('table');
            })
            .catch(error => {
                return [];
            });
    }

    function getFirstDigits(inputString) {
        const firstDigits = inputString.match(/\d+/);

        if (firstDigits !== null) {
            return firstDigits[0];
        } else {
            return -1;
        }
    }

    // Function to display a toast message
    function showToast(message) {
        const toast = document.createElement('div');
        toast.textContent = message;
        toast.style.cssText = `
            position: fixed;
            bottom: 10px;
            left: 50%;
            transform: translateX(-50%);
            background-color: #333;
            color: #fff;
            padding: 10px 20px;
            border-radius: 5px;
        `;
        document.body.appendChild(toast);
        setTimeout(() => {
            toast.remove();
        }, config.toastDuration); // Display for 3 seconds
    }

    // Function to update the loading indicator with data points
    function updateLoadingProgress(dataPointsLoaded) {
        loadingProgress.textContent = `Downloading page ${dataPointsLoaded}`;
    }

    // Function to copy all tables to clipboard
    function copyAllTablesToClipboard() {
        let clipboard = '';
        let allMarkdown = '';
        let allXWiki = '';

        // Find and process all HTML tables on the page
        document.querySelectorAll('table').forEach((table, tableIndex) => {
            downloadCSV(table, tableIndex);
            const markdown = tableToMarkdown(table);
            allMarkdown += markdown + '\n---\n'; // Add a separator between tables
            const xwiki = tableToXWiki(table);
            allXWiki += xwiki + '\n----\n'; // Add a separator between tables
        });

        clipboard += 'Markdown Tables: \n\n' + allMarkdown + '\n';
        clipboard += 'XWiki Tables: \n\n' + allXWiki + '\n';

        GM_setClipboard(clipboard);

        // Display a toast notification
        showToast('Markdown/XWiki table copied to clipboard!');
    }


    // Add a keyboard shortcut to copy all tables (e.g., Ctrl + Shift + C)
    window.addEventListener('keydown', event => {
        if (event.ctrlKey && event.shiftKey && event.key === 'C') {
            copyAllTablesToClipboard();
            event.preventDefault();
        }
    });

    // Create a Mutation Observer to watch for changes in the DOM
    const observer = new MutationObserver(function(mutations) {
        // Check if the tables you're looking for are now available in the DOM
        if (document.querySelectorAll('table').length > 0) {
            // Disconnect the observer to stop watching for changes
            observer.disconnect();

            setTimeout(() => {
                processTables();
            }, config.waitForDOMTimeout);
        }
    });

    // Start observing changes in the DOM
    observer.observe(document.body, { childList: true, subtree: true });

    function processTables() {
        // Find and process all HTML tables on the page
        document.querySelectorAll('table').forEach((table, tableIndex) => {
            const computedStyle = window.getComputedStyle(table);
            if (computedStyle.display === 'none') {
                return;
            }

            const markdown = tableToMarkdown(table);
            const xwiki = tableToXWiki(table);

            // Create a markdown button element
            const markdownButton = document.createElement('button');
            markdownButton.className = 'clipboard-button'; // Add a CSS class for styling
            markdownButton.innerHTML = '<i class="fa fa-clipboard" aria-hidden="true"></i> Copy Markdown Table to Clipboard';

            // Create a xwiki button element
            const xwikiButton = document.createElement('button');
            xwikiButton.className = 'clipboard-button'; // Add a CSS class for styling
            xwikiButton.innerHTML = '<i class="fa fa-clipboard" aria-hidden="true"></i> Copy XWiki Table to Clipboard';

            // Create a csv button element
            const csvButton = document.createElement('button');
            csvButton.className = 'clipboard-button'; // Add a CSS class for styling
            csvButton.innerHTML = '<i class="fa fa-clipboard" aria-hidden="true"></i> Download CSV';

            // Create a csv button element
            const csvAllButton = document.createElement('button');
            csvAllButton.className = 'clipboard-button'; // Add a CSS class for styling
            csvAllButton.innerHTML = '<i class="fa fa-clipboard" aria-hidden="true"></i> Download CSV All Pages';

            // Add a click event listener to the button
            markdownButton.addEventListener('click', () => {
                GM_setClipboard(markdown);
                showToast('Markdown table copied to clipboard!');
            });

            // Add a click event listener to the button
            xwikiButton.addEventListener('click', () => {
                GM_setClipboard(xwiki);
                showToast('XWiki table copied to clipboard!');
            });

            // Add a click event listener to the button
            csvButton.addEventListener('click', () => {
                if (table) {
                    downloadCSV(table, tableIndex);
                } else {
                    showToast('No table found on this page.');
                }
            });

            // Add a click event listener to the button
            csvAllButton.addEventListener('click', () => {
                if (table) {
                    document.body.appendChild(loadingContainer);
                    downloadCSVAllPages(tableIndex);
                } else {
                    showToast('No table found on this page.');
                }
            });

            // Append the button under the table
            const container = document.createElement('div');
            container.appendChild(markdownButton);
            container.appendChild(xwikiButton);
            container.appendChild(csvButton);
            if (a != null) {
                container.appendChild(csvAllButton);
            }
            table.parentNode.insertBefore(container, table.nextSibling);
        });
    }

    // Add custom CSS styles for your UI elements (customize as needed)
    const styles = `
    .markdown-table-container {
        margin-bottom: 20px;
        border: 1px solid #ccc;
        padding: 10px;
    }

    .clipboard-button {
        display: inline-block;
        padding: 8px 16px;
        font-size: 12px;
        font-weight: bold;
        text-align: center;
        text-decoration: none;
        border: none;
        border-radius: 5px;
        cursor: pointer;
        transition: background-color 0.3s, color 0.3s;
        margin-top: 10px;
        margin-right: 10px;
        margin-bottom: 10px;
    }

    .clipboard-button:hover {
        background-color: #0056b3; /* Hover background color */
        color: #fff; /* Hover text color */
    }


    .clipboard-button i {
        margin-right: 5px;
    }

    #loading-container {
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        display: flex;
        flex-direction: column;
        align-items: center;
    }
    
    #loading-circle {
        border: 4px solid transparent;
        border-top: 4px solid #007BFF; /* Loading circle color */
        border-radius: 50%;
        width: 40px;
        height: 40px;
        animation: spin 1s linear infinite;
        margin-bottom: 10px;
    }
    
    #loading-progress {
        font-size: 12px;
    }
    
    @keyframes spin {
        0% { transform: rotate(0deg); }
        100% { transform: rotate(360deg); }
    }
        
    .toast {
        position: fixed;
        bottom: 20px;
        right: 20px;
        background-color: #333;
        color: #fff;
        padding: 10px;
        border-radius: 4px;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
        z-index: 9999;
    }`;
    // Create a <style> element and add the custom styles
    const styleElement = document.createElement('style');
    styleElement.textContent = styles;
    document.head.appendChild(styleElement);
})();