Table copier (to clipboard)

Choose from your userscipts addon menu. All tables are highlighted, click one to copy, elsewhere to cancel. Copy table and paste into spreadsheets like Excel, Google Sheets, LibreOffice Calc, OpenOffice Calc and others.

< 脚本 Table copier (to clipboard) 的反馈

评价:好评 - 脚本运行良好

§
发布于:2025-03-20

I will FULLY admit that this is generated by an llm. But it seems to work by my testing.

I love your userscript but personally I sometimes find myself needing a tsv/csv or markdown table version of a table.
So this version of the script does that.
Feel free to copy it and update your script. If you prefer not to, that's fine, I can make a fork and change it in my fork.


(function() {
'use strict';

if (!('setClipboard' in GM)) return;

// Global "mode" determines how we process or output the table content.
let mode = null; // can be "html0", "html1", "html2", "html3", "csv", "tsv", "md"
let tables = [];
let tablesBorders = new Map();

/**
* 1) The main "run" function highlights all tables.
* If there's only one table, copy immediately (no click needed).
*/
function run() {
tables = [...document.getElementsByTagName('table')];
if (tables.length === 1) {
// If there's exactly one table, copy without waiting for clicks.
borderHighlight(tables[0]);
copyTable(tables[0]);
tables[0].scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
setTimeout(borderNormal, 1000, tables[0]);
} else {
// If there's more than one, highlight them all and wait for user to click.
tables.forEach(elem => {
borderHighlight(elem);
elem.addEventListener('click', takeElem, {capture: true});
elem.addEventListener('contextmenu', takeElem, {capture: true});
});
document.addEventListener('click', stopAll);
}
}

/**
* 2) The "end" function resets everything (removes highlights, events, etc.)
*/
function end() {
tables.forEach(elem => {
borderNormal(elem);
elem.removeEventListener('click', takeElem, {capture: true});
elem.removeEventListener('contextmenu', takeElem, {capture: true});
});
document.removeEventListener('click', stopAll);
tables = [];
tablesBorders = new Map();
}

/**
* 3) If user clicks anywhere else, we cancel the operation.
*/
function stopAll(e) {
e.preventDefault();
e.stopPropagation();
end();
}

/**
* 4) On a table click, copy that table; then end.
*/
function takeElem(e) {
stopAll(e);
const table = e.target.tagName === 'TABLE' ? e.target : e.target.closest('table');
if (table) copyTable(table);
}

/**
* 5) Highlight the table with a dashed green border.
*/
function borderHighlight(elem) {
tablesBorders.set(elem, elem.style.border);
elem.style.border = '2px dashed green';
}

/**
* 6) Restore the table's original border style.
*/
function borderNormal(elem) {
elem.style.border = tablesBorders.get(elem) || '';
}

/**
* 7) Prepare the table by removing certain elements or cleaning up tags.
* This is used only for HTML-based copying (mode: html0..html3).
*/
function processTableForHTML(table, cleanedLevel) {
// clone the table so we don't affect the page's real table
let clone = table.cloneNode(true);

// remove br, wbr, hr, img, style
clone.querySelectorAll('br, wbr, hr, img, style')
.forEach(elem => elem.remove());

// remove or convert links if needed
if (cleanedLevel >= 2) {
// replace with containing the href
clone.querySelectorAll('a').forEach(link => {
const span = document.createElement('span');
span.textContent = link.href;
link.replaceWith(span);
// add space after the link
span.after(' ');
});
}
// if cleanedLevel === 3, convert nearly all tags to except table/row/cell tags
if (cleanedLevel === 3) {
clone.querySelectorAll('*').forEach(elem => {
if (['SPAN','TBODY','THEAD','TFOOT','TR','TH','TD','CAPTION','COLGROUP','COL'].includes(elem.tagName)) {
return;
}
const span = document.createElement('span');
while (elem.firstChild) {
span.appendChild(elem.firstChild);
}
elem.replaceWith(span);
});
} else {
// if cleanedLevel is 1 or 2, we just unify p/div/hX and list elements into
clone.querySelectorAll('p, div, h1, h2, h3, h4, h5, h6, ul, li, ol, dl, dt, dd')
.forEach(elem => {
const span = document.createElement('span');
while (elem.firstChild) {
span.appendChild(elem.firstChild);
}
// copy attributes
for (let attr of elem.getAttributeNames()) {
span.setAttribute(attr, elem.getAttribute(attr));
}
elem.replaceWith(span);
});
}
return clone;
}

/**
* 8) Convert into a 2D array (array of rows, each row is array of cell text).
*/
function tableTo2DArray(table) {
let result = [];
// For each row ...
for (let row of table.querySelectorAll('tr')) {
let rowData = [];
// For each cell

or ...
let cells = row.querySelectorAll('th, td');
for (let cell of cells) {
// get text content and trim extra whitespace
rowData.push(cell.textContent.trim());
}
result.push(rowData);
}
return result;
}

/**
* 9) Convert 2D array into CSV
*/
function arrayToCSV(data) {
// escape " by doubling them, and wrap in quotes if needed
// Example: "foo,bar" -> "\"foo,bar\""
return data.map(
row => row.map(
cell => {
let c = cell.replace(/"/g, '""');
// If cell has commas or quotes, enclose in quotes.
if (/[",]/.test(c)) {
c = `"${c}"`;
}
return c;
}
).join(',')
).join('\n');
}

/**
* 10) Convert 2D array into TSV
*/
function arrayToTSV(data) {
return data.map(row =>
row.map(cell => cell.replace(/\t/g, ' ')).join('\t')
).join('\n');
}

/**
* 11) Convert 2D array into Markdown table.
* Basic approach:
* - The first row is the header.
* - Next row is a separator with dashes.
* - The rest are normal rows.
*/
function arrayToMarkdown(data) {
if (!data || data.length === 0) {
return '';
}
let lines = [];
// First row as header
let header = data[0];
lines.push('| ' + header.join(' | ') + ' |');
// Separator row
let separator = header.map(_ => '---');
lines.push('| ' + separator.join(' | ') + ' |');
// Rest of the rows
for (let i = 1; i < data.length; i++) {
lines.push('| ' + data[i].join(' | ') + ' |');
}
return lines.join('\n');
}

/**
* 12) Copy the table (depending on mode).
*/
function copyTable(table) {
let finalText = '';
let param = null; // for GM.setClipboard

if (mode.startsWith('html')) {
// For "html0", "html1", "html2", "html3" we copy HTML
// The digit after "html" is the "cleaned" level
let cleanedLevel = parseInt(mode.slice(-1), 10); // 0..3
let clone = (cleanedLevel === 0) ? table : processTableForHTML(table, cleanedLevel);
// Tampermonkey requires `html`, Violentmonkey/Userscripts require `text/html`.
switch (GM.info.scriptHandler) {
case 'Tampermonkey':
param = 'html';
break;
case 'Violentmonkey':
case 'Userscripts':
param = 'text/html';
break;
case 'Greasemonkey':
default:
param = null;
}
finalText = clone.outerHTML;
}
else {
// For csv / tsv / md, we parse into 2D array first
const data = tableTo2DArray(table);
if (mode === 'csv') {
finalText = arrayToCSV(data);
} else if (mode === 'tsv') {
finalText = arrayToTSV(data);
} else if (mode === 'md') {
finalText = arrayToMarkdown(data);
}
// For these, we want plain text
param = 'text/plain';
}

if (param) {
GM.setClipboard(finalText, param);
} else {
// fallback
GM.setClipboard(finalText);
}
// Optionally, remove or comment out any alerts:
// alert('Table copied to clipboard!');
}

// Register Menu Commands for various modes

// -- HTML modes (existing ones)
GM.registerMenuCommand('Copy table (full HTML)', () => { mode = 'html0'; run();});
GM.registerMenuCommand('Copy table (cleaned HTML)', () => { mode = 'html1'; run();});
GM.registerMenuCommand('Copy table (cleaned, links as hrefs)', () => { mode = 'html2'; run();});
GM.registerMenuCommand('Copy table (cleanest, links as hrefs)', () => { mode = 'html3'; run();});

// -- New modes: CSV, TSV, Markdown
GM.registerMenuCommand('Copy table as CSV', () => { mode = 'csv'; run();});
GM.registerMenuCommand('Copy table as TSV', () => { mode = 'tsv'; run();});
GM.registerMenuCommand('Copy table as Markdown', () => { mode = 'md'; run();});

})();

发布留言

登录以发布留言。