table-copier

适用于任意网站,快速复制表格为纯文本、HTML、图片

  1. // ==UserScript==
  2. // @name table-copier
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.6
  5. // @description 适用于任意网站,快速复制表格为纯文本、HTML、图片
  6. // @match *://*/*
  7. // @require https://cdn.staticfile.org/html2canvas/1.4.1/html2canvas.min.js
  8. // @grant none
  9. // @run-at document-idle
  10. // @license GPL-3.0-only
  11. // @create 2023-06-27
  12. // ==/UserScript==
  13.  
  14.  
  15. (function() {
  16. "use strict";
  17.  
  18. const SCRIPTS = [
  19. ["html2canvas", "https://cdn.staticfile.org/html2canvas/1.4.1/html2canvas.min.js"]
  20. ];
  21. const BTN = `<button class="table-copier-btn" style="width: 70px; height: 30px;" onclick="copy_table(this)">复制表格</button>`;
  22. const BOOT_DELAY = 2000;
  23.  
  24. /**
  25. * 元素选择器
  26. * @param {string} selector
  27. * @returns {Array<HTMLElement>}
  28. */
  29. function $(selector) {
  30. const self = this?.querySelectorAll ? this : document;
  31. return [...self.querySelectorAll(selector)];
  32. }
  33.  
  34. /**
  35. * 加载CDN脚本
  36. * @param {string} url
  37. */
  38. async function load_script(url) {
  39. try {
  40. const code = await (await fetch(url)).text();
  41. Function(code)();
  42. } catch(e) {
  43. return new Promise(resolve => {
  44. console.error(e);
  45. // 嵌入<script>方式
  46. const script = document.createElement("script");
  47. script.src = url;
  48. script.onload = resolve;
  49. document.body.appendChild(script);
  50. });
  51. }
  52. }
  53.  
  54. async function until_scripts_loaded() {
  55. return gather(SCRIPTS.map(
  56. // kv: [prop, url]
  57. kv => (async () => {
  58. if (window[kv[0]]) return;
  59. await load_script(kv[1]);
  60. })()
  61. ));
  62. }
  63.  
  64. /**
  65. * 等待全部任务落定后返回值的列表
  66. * @param {Iterable<Promise>} tasks
  67. * @returns {Promise<Array>} values
  68. */
  69. async function gather(tasks) {
  70. const results = await Promise.allSettled(tasks);
  71. const filtered = [];
  72. for (const result of results) {
  73. if (result.value) {
  74. filtered.push(result.value);
  75. }
  76. }
  77. return filtered;
  78. }
  79.  
  80. /**
  81. * 递归的修正表内元素
  82. * @param {HTMLElement} elem
  83. */
  84. function adjust_table(elem) {
  85. for (const child of elem.children) {
  86. adjust_table(child);
  87.  
  88. for (const attr of child.attributes) {
  89. // 链接补全
  90. const name = attr.name;
  91. if (["src", "href"].includes(name)) {
  92. child.setAttribute(name, child[name]);
  93. }
  94. }
  95. }
  96. }
  97.  
  98. /**
  99. * canvas转blob
  100. * @param {HTMLCanvasElement} canvas
  101. * @param {string} type
  102. * @returns {Promise<Blob>}
  103. */
  104. function canvas_to_blob(canvas) {
  105. return new Promise(
  106. resolve => canvas.toBlob(resolve, "image/png")
  107. );
  108. }
  109.  
  110. /**
  111. * @param {HTMLTableCellElement} cell
  112. * @returns {string}
  113. */
  114. function cell_to_text(cell) {
  115. return cell
  116. .textContent
  117. .replace(/\n/g, " ")
  118. .replace(/\t/g, " ")
  119. .trim();
  120. // const children = cell.children;
  121. // if (children.length === 0) {
  122. // return cell
  123. // .textContent
  124. // .replace(/\n/g, " ")
  125. // .replace(/\t/g, " ")
  126. // .trim();
  127. // }
  128.  
  129. // return [...children].map(
  130. // (child) => cell_to_text(child, depth + 1)
  131. // ).join(" ".repeat(depth));
  132. }
  133.  
  134. /**
  135. * 表格转tsv字符串
  136. * @param {HTMLTableElement} table
  137. * @returns {string}
  138. */
  139. function table_to_tsv(table) {
  140. return [...table.rows].map((row) => {
  141. return [...row.cells].map((cell) => {
  142. return cell_to_text(cell);
  143. }).join("\t")
  144. }).join("\n");
  145. }
  146.  
  147. /**
  148. * 以包含变量名getter属性的对象输出到控制台,延迟计算
  149. * @param {*} obj
  150. */
  151. function log_as_getter(obj) {
  152. const name = Object.getOwnPropertyNames(obj)[0];
  153. const getter = {
  154. get [name]() {
  155. return obj[name];
  156. }
  157. };
  158. console.log(name + ":", getter);
  159. }
  160.  
  161. /**
  162. * @param {HTMLTableElement} table
  163. * @returns {Promise<Blob>}
  164. */
  165. async function table_to_text_blob(table) {
  166. console.log("table to text");
  167. // table 转 tsv 格式文本
  168. const text = table_to_tsv(table);
  169. log_as_getter({ text });
  170. return new Blob([text], { type: "text/plain" });
  171. }
  172.  
  173. /**
  174. * @param {HTMLTableElement} table
  175. * @returns {Promise<Blob>}
  176. */
  177. async function table_to_html_blob(table) {
  178. console.log("table to html");
  179.  
  180. const _table = table.cloneNode(true);
  181. adjust_table(_table);
  182. return new Blob([_table.outerHTML], { type: "text/html" });
  183. }
  184.  
  185. /**
  186. * @param {HTMLTableElement} table
  187. * @returns {Promise<Blob>}
  188. */
  189. async function table_to_image_blob(table) {
  190. console.log("table to image");
  191.  
  192. let canvas;
  193. try {
  194. canvas = await window.html2canvas(table);
  195. } catch(e) {
  196. console.error(e);
  197. }
  198. log_as_getter({ canvas });
  199. if (!canvas) return;
  200.  
  201. return canvas_to_blob(canvas);
  202. }
  203.  
  204. /**
  205. * 使用过时的 execCommand 复制文本
  206. * @param {string} text
  207. * @returns {Promise<string>}
  208. */
  209. async function copy_text_legacy(text) {
  210. return new Promise(resolve => {
  211. document.oncopy = event => {
  212. event.clipboardData.setData("text/plain", text);
  213. event.preventDefault();
  214. resolve();
  215. };
  216. document.execCommand("copy");
  217. });
  218. }
  219.  
  220. /**
  221. * @param {Array<Blob>} blobs
  222. * @returns {Object}
  223. */
  224. function merge_blobs_to_bundle(blobs) {
  225. const bundle = Object.create(null);
  226. for (const blob of blobs) {
  227. bundle[blob.type] = blob;
  228. }
  229. return bundle;
  230. }
  231.  
  232. /**
  233. * @param {Array<Blob>} blobs
  234. * @returns {Promise<void>}
  235. */
  236. function copy(blobs) {
  237. const bundle = merge_blobs_to_bundle(blobs);
  238. const item = new ClipboardItem(bundle);
  239. log_as_getter({ bundle });
  240. log_as_getter({ item });
  241. return navigator.clipboard.write([item]);
  242. }
  243. /**
  244. * @param {HTMLTableElement} table
  245. * @returns {Promise<void>}
  246. */
  247. async function copy_table_as_multi_types(table) {
  248. const converts = [
  249. table_to_text_blob,
  250. table_to_html_blob,
  251. table_to_image_blob,
  252. ];
  253. const blobs = await gather(converts.map(
  254. convert => convert(table)
  255. ));
  256.  
  257. try {
  258. await copy(blobs);
  259. alert("复制成功!");
  260.  
  261. } catch(e) {
  262. console.error(e);
  263. alert("复制失败!");
  264. }
  265. }
  266.  
  267. /**
  268. * @param {HTMLTableElement} table
  269. * @returns {Promise<void>}
  270. */
  271. async function copy_table_as_text_legacy(table) {
  272. try {
  273. await copy_text_legacy(table_to_tsv(table));
  274. alert("复制成功!");
  275. } catch(e) {
  276. console.error(e);
  277. alert("复制失败!");
  278. }
  279. }
  280.  
  281. /**
  282. * 异步的复制表格到剪贴板
  283. * @param {HTMLButtonElement} btn
  284. * @param {boolean} ret_val 是否返回值(tsv)而不是复制到剪贴板(默认 false)
  285. * @returns {Promise<null | string>}
  286. *
  287. */
  288. async function copy_table(btn, ret_val=false) {
  289. const table = btn.closest("table");
  290. if (!table) {
  291. alert("出错了:按钮外部没有表格");
  292. return;
  293. }
  294.  
  295. if (ret_val) {
  296. return table_to_tsv(table);
  297. }
  298.  
  299. // 移除按钮
  300. $(".table-copier-btn").forEach(
  301. btn => btn.remove()
  302. );
  303. // 复制表格
  304. if (!navigator.clipboard) {
  305. await copy_table_as_text_legacy(table);
  306. } else {
  307. await copy_table_as_multi_types(table);
  308. }
  309. // 增加按钮
  310. add_btns();
  311. return null;
  312. }
  313.  
  314. function add_btns() {
  315. for (const table of $("table")) {
  316. // 跳过隐藏的表格
  317. if (!table.getClientRects()[0]) continue;
  318. table.insertAdjacentHTML("afterbegin", BTN);
  319. }
  320. }
  321.  
  322. async function main() {
  323. try {
  324. await until_scripts_loaded();
  325. } catch(e) {
  326. console.error(e);
  327. }
  328.  
  329. window.copy_table = copy_table;
  330. add_btns();
  331.  
  332. // 递归的注入自身到iframe
  333. $("iframe").forEach(iframe => {
  334. try {
  335. iframe.contentWindow.eval(main.toString());
  336. } catch(e) {}
  337. });
  338. };
  339.  
  340. setTimeout(main, BOOT_DELAY);
  341. })();