保存页面

将页面保存为单个 HTML 文件。

  1. // ==UserScript==
  2. // @name Save Page
  3. // @name:en Save Page
  4. // @name:zh-CN 保存页面
  5. // @description Save page as single HTML file.
  6. // @description:en Save page as single HTML file.
  7. // @description:zh-CN 将页面保存为单个 HTML 文件。
  8. // @namespace https://gf.qytechs.cn/users/197529
  9. // @version 0.1.7
  10. // @author kkocdko
  11. // @license Unlicense
  12. // @match *://*/*
  13. // @grant GM_xmlhttpRequest
  14. // @noframes
  15. // ==/UserScript==
  16. "use strict";
  17.  
  18. const { addFloatButton, fetchex } = {
  19. addFloatButton(text, onclick) /* 20220509-1936 */ {
  20. if (!document.addFloatButton) {
  21. const host = document.body.appendChild(document.createElement("div"));
  22. const root = host.attachShadow({ mode: "open" });
  23. root.innerHTML = `<style>:host{position:fixed;top:4px;left:4px;z-index:2147483647;height:0}#i{display:none}*{float:left;padding:0 1em;margin:4px;font-size:14px;line-height:2em;color:#fff;user-select:none;background:#28e;border:1px solid #fffa;border-radius:8px;transition:.3s}[for]~:active{filter:brightness(1.1);transition:0s}:checked~*{opacity:.3;transform:translateY(-3em)}:checked+*{transform:translateY(3em)}</style><input id=i type=checkbox><label for=i>&zwj;</label>`;
  24. document.addFloatButton = (text, onclick) => {
  25. const el = document.createElement("label");
  26. el.textContent = text;
  27. el.addEventListener("click", onclick);
  28. return root.appendChild(el);
  29. };
  30. }
  31. return document.addFloatButton(text, onclick);
  32. },
  33. fetchex(url, type) /* 20220509-1838 */ {
  34. // @grant GM_xmlhttpRequest
  35. if (self.GM_xmlhttpRequest)
  36. return new Promise((resolve, onerror) => {
  37. const onload = (e) => resolve(e.response);
  38. GM_xmlhttpRequest({ url, responseType: type, onload, onerror });
  39. });
  40. else return fetch(url).then((v) => v[type]());
  41. },
  42. };
  43.  
  44. // TODO: Content Security Policy. Example: https://github.com/kkocdko/kblog
  45.  
  46. addFloatButton("Remove images", async function () {
  47. const placeholder = `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"></svg>`;
  48. document.querySelectorAll("img").forEach((el) => {
  49. el.src = placeholder;
  50. });
  51. this.textContent = "Images removed";
  52. this.style.background = "#4caf50";
  53. });
  54.  
  55. addFloatButton("Save page", async function () {
  56. console.time("save page");
  57. this.style.background = "#ff9800";
  58. const interval = setInterval((o) => {
  59. const suffix = ".".padStart((++o.i % 3) + 1, " ").padEnd(3, " ");
  60. this.innerHTML = "Saving " + suffix.replace(/\s/g, "&nbsp;");
  61. }, ...[333, { i: 0 }]); // 茴回囘囬
  62.  
  63. const /** @type {Document} */ dom = document.cloneNode(true);
  64.  
  65. const removeList = `script, style, source, title, link`;
  66. dom.querySelectorAll(removeList).forEach((el) => el.remove());
  67.  
  68. const qsam = (s, f) => [...document.querySelectorAll(s)].map(f);
  69.  
  70. const imgs = dom.querySelectorAll("img");
  71. const imgTasks = qsam("img", async (el, i) => {
  72. const reader = new FileReader();
  73. reader.readAsDataURL(await fetchex(el.currentSrc, "blob"));
  74. await new Promise((r) => (reader.onload = reader.onerror = r));
  75. imgs[i].src = reader.result;
  76. imgs[i].srcset = "";
  77. });
  78.  
  79. const css = []; // Keep order
  80. const cssTasks = qsam("style, link[rel=stylesheet]", async (el, i) => {
  81. if (el.tagName === "STYLE") css[i] = el.textContent;
  82. else css[i] = await fetchex(el.href, "text");
  83. });
  84.  
  85. await Promise.allSettled([...imgTasks, ...cssTasks]);
  86.  
  87. // [TODO:Limitation] `url()` and `image-set()` in css will not be save
  88. // Avoid the long-loading issue
  89. const cssStr = css
  90. .filter((v) => !v.slice(0, 128).includes("<!DOCTYPE")) // exclude some invalid resources
  91. .join("\n\n")
  92. .replace(/(url|image-set)(.+?)/g, "url()");
  93. dom.head.appendChild(dom.createElement("style")).textContent = cssStr;
  94. console.log({ cssStr });
  95.  
  96. dom.querySelectorAll("a").forEach((el) => {
  97. // "a:not([href^='http']):not([href^='//'])"
  98. el.setAttribute("href", el.href); // make links absolute
  99. });
  100.  
  101. // [TODO:Limitation] breaked some no-doctype / xhtml / html4 pages
  102. const result = "<!DOCTYPE html>" + dom.documentElement.outerHTML;
  103.  
  104. console.timeEnd("save page");
  105.  
  106. clearInterval(interval);
  107.  
  108. const link = document.createElement("a"); // Using `dom` will cause failure
  109. link.download = `${document.title}_${Date.now()}.html`;
  110. link.href = "data:text/html," + encodeURIComponent(result);
  111. link.click();
  112.  
  113. this.textContent = "Page saved";
  114. this.style.background = "#4caf50";
  115. });

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址