Convert HTML tables to Markdown/XWiki/CSV format
// ==UserScript==
// @name HTML Table to Markdown/XWiki/CSV Converter
// @namespace tungxd301
// @version 1.1
// @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: 3000,
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);
})();