MemTrace

trace browsing history and preserve tables (HTML passthrough)

  1. // ==UserScript==
  2. // @name MemTrace
  3. // @namespace Violentmonkey Scripts
  4. // @version 0.6
  5. // @description trace browsing history and preserve tables (HTML passthrough)
  6. // @author fankaidev
  7. // @match *://*/*
  8. // @exclude *://cubox.pro/*
  9. // @exclude *://localhost:*/*
  10. // @exclude *://127.0.0.1:*/*
  11. // @grant GM_xmlhttpRequest
  12. // @grant GM_setValue
  13. // @grant GM_getValue
  14. // @grant GM_registerMenuCommand
  15. // @require https://cdnjs.cloudflare.com/ajax/libs/turndown/7.1.1/turndown.min.js
  16. // @require https://unpkg.com/turndown-plugin-gfm@1.0.2/dist/turndown-plugin-gfm.js
  17. // @require https://cdn.jsdelivr.net/npm/dompurify@3.1.5/dist/purify.min.js
  18. // @require https://cdn.jsdelivr.net/npm/crypto-js@4.1.1/crypto-js.js
  19. // @require https://cdn.jsdelivr.net/npm/@mozilla/readability@0.5.0/Readability.min.js
  20. // @license MIT
  21. // ==/UserScript==
  22. (function () {
  23. "use strict";
  24. function md5(input) {
  25. return CryptoJS.MD5(input).toString();
  26. }
  27. let hrefHistory = [];
  28. // Function to get or initialize global state
  29. function getGlobalState(key, defaultValue) {
  30. return GM_getValue(key, defaultValue);
  31. }
  32. // Function to update global state
  33. function updateGlobalState(key, value) {
  34. GM_setValue(key, value);
  35. }
  36. // Function to get the endpoint
  37. function getEndpoint() {
  38. let endpoint = getGlobalState("endpoint", null);
  39. if (!endpoint) {
  40. endpoint = prompt("[MemTrace] Please enter the endpoint URL:", "https://api.example.com/endpoint");
  41. if (endpoint) {
  42. updateGlobalState("endpoint", endpoint);
  43. } else {
  44. console.error("[MemTrace] No endpoint provided. Script will not function correctly.");
  45. }
  46. }
  47. return endpoint;
  48. }
  49. // Function to change the endpoint
  50. function changeEndpoint() {
  51. let newEndpoint = prompt("[MemTrace] Enter new endpoint URL:", getGlobalState("endpoint", ""));
  52. if (newEndpoint) {
  53. updateGlobalState("endpoint", newEndpoint);
  54. console.log("[MemTrace] Endpoint updated to", newEndpoint);
  55. }
  56. }
  57. // Register menu command to change endpoint
  58. GM_registerMenuCommand("Change MemTrace Endpoint", changeEndpoint);
  59. function processPage() {
  60. const article = new Readability(document.cloneNode(true)).parse().content;
  61. // console.log("article", article);
  62. const turndownService = new TurndownService({
  63. keepReplacement: function (content, node) {
  64. return node.isBlock ? "\n\n" + node.outerHTML + "\n\n" : node.outerHTML;
  65. },
  66. });
  67. // Add a rule to keep tables
  68. turndownService.addRule("tables", {
  69. filter: ["table"],
  70. replacement: function (content, node) {
  71. return node.outerHTML;
  72. },
  73. });
  74. // Uncomment the following line if you want to use the GFM table plugin instead
  75. // turndownService.use(turndownPluginGfm.tables);
  76. return turndownService.turndown(article);
  77. }
  78. function savePage(markdown) {
  79. const url = window.location.href.split("#")[0];
  80. let data = {
  81. title: document.title,
  82. source: "chrome",
  83. id: md5(url),
  84. markdown: markdown,
  85. url: url,
  86. };
  87. console.log("[MemTrace] saving page", data);
  88. GM_xmlhttpRequest({
  89. method: "POST",
  90. url: getEndpoint(),
  91. data: JSON.stringify(data),
  92. headers: {
  93. "Content-Type": "application/json",
  94. },
  95. onload: function (response) {
  96. if (response.status === 200) {
  97. console.log("[MemTrace] saved page");
  98. } else {
  99. console.error("Failed to save to MemTrace", response.responseText);
  100. }
  101. },
  102. onerror: function (error) {
  103. console.error("Request failed:", error);
  104. },
  105. });
  106. }
  107. function parseRedditReply(reply, depth) {
  108. let replyText = "";
  109. replyText += "\n---\n";
  110. const ts = new Date(reply.data.created * 1000).toISOString();
  111. replyText += ">".repeat(depth) + ` **${reply.data.author}** - ${ts}\n`;
  112. replyText += ">".repeat(depth) + "\n";
  113. const lines = reply.data.body.split("\n");
  114. for (const line of lines) {
  115. replyText += ">".repeat(depth) + " " + line + "\n";
  116. }
  117. if (!reply.data.replies) {
  118. return replyText;
  119. }
  120. for (const child of reply.data.replies.data.children) {
  121. replyText += parseRedditReply(child, depth + 1);
  122. }
  123. return replyText;
  124. }
  125. function processRedditPage() {
  126. console.log("[MemTrace] processing reddit page");
  127. fetch(window.location.href + ".json")
  128. .then((response) => response.json())
  129. .then((responseJson) => {
  130. const page = responseJson;
  131. const post = page[0].data.children[0].data;
  132. const ts = new Date(post.created * 1000).toISOString();
  133. let markdown = `*${post.subreddit_name_prefixed}*\n\n`;
  134. markdown += `**${post.author}** - ${ts}\n\n`;
  135. markdown += `${post.selftext}\n\n`;
  136. for (const reply of page[1].data.children) {
  137. markdown += parseRedditReply(reply, 1);
  138. }
  139. savePage(markdown);
  140. });
  141. }
  142. function process() {
  143. const url = window.location.href.split("#")[0];
  144. if (hrefHistory.includes(url)) {
  145. console.log("[MemTrace] skip processed url", url);
  146. return;
  147. }
  148. console.log("[MemTrace] processing url", url);
  149. hrefHistory.push(url);
  150. if (/reddit.com\/r\/[^/]+\/comments/.test(url)) {
  151. processRedditPage();
  152. } else {
  153. const markdown = processPage();
  154. if (markdown.length < 100) {
  155. console.log("[MemTrace] fail to parse page");
  156. return;
  157. }
  158. savePage(markdown);
  159. }
  160. }
  161. function scheduleProcess() {
  162. if (document.contentType != "text/html") {
  163. return;
  164. }
  165. if (window.self === window.top) {
  166. console.log(`[MemTrace] type=${document.contentType} href=${window.location.href}`);
  167. setTimeout(() => {
  168. process();
  169. }, 5000);
  170. }
  171. }
  172. // Intercept pushState and replaceState
  173. const originalPushState = history.pushState;
  174. const originalReplaceState = history.replaceState;
  175. history.pushState = function () {
  176. originalPushState.apply(this, arguments);
  177. scheduleProcess();
  178. };
  179. history.replaceState = function () {
  180. originalReplaceState.apply(this, arguments);
  181. scheduleProcess();
  182. };
  183. window.addEventListener("load", function () {
  184. scheduleProcess();
  185. });
  186. window.addEventListener("popstate", function (event) {
  187. scheduleProcess();
  188. });
  189. })();