4d4y Markdown Enhancer

Convert potential Markdown syntax into HTML in 4d4y forum posts without removing existing HTML elements. Toggle original text with Ctrl+M, with a mode switch notification.

当前为 2025-02-22 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name 4d4y Markdown Enhancer
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.5
  5. // @description Convert potential Markdown syntax into HTML in 4d4y forum posts without removing existing HTML elements. Toggle original text with Ctrl+M, with a mode switch notification.
  6. // @match https://www.4d4y.com/forum/*
  7. // @author 屋大维 + ChatGPT
  8. // @license MIT
  9. // @grant none
  10. // @run-at document-end
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. "use strict";
  15.  
  16. window.copyCode = function (button) {
  17. let codeElement = button.parentElement.querySelector("pre code");
  18. if (!codeElement) return;
  19.  
  20. let text = codeElement.innerText;
  21. navigator.clipboard.writeText(text).then(() => {
  22. button.innerText = "已复制!";
  23. setTimeout(() => (button.innerText = "复制"), 1500);
  24. });
  25. };
  26.  
  27. function markdownToHtml(md) {
  28. if (!md) return "";
  29.  
  30. let blocks = {};
  31. let blockIndex = 0;
  32.  
  33. // **1. 处理带语言标签的代码块**
  34. md = md.replace(/```(\w+)\s*<br>\s*([\s\S]*?)```/g, (match, lang, code) => {
  35. let placeholder = `%%CODE${blockIndex}%%`;
  36. let cleanCode = code.replace(/<br\s*\/?>/g, "").trim();
  37.  
  38. let langLabel = `<div style="
  39. background-color: #3a3f4b;
  40. color: #ffffff;
  41. font-size: 12px;
  42. font-weight: bold;
  43. padding: 6px 12px;
  44. border-top-left-radius: 6px;
  45. font-family: sans-serif;
  46. display: inline-block;
  47. min-width: 100px;
  48. text-align: left;
  49. ">${lang}</div>`;
  50.  
  51. let copyButton = `<button onclick="copyCode(this)" style="
  52. position: absolute;
  53. top: 6px;
  54. right: 10px;
  55. background-color: transparent;
  56. border: none;
  57. color: #ffffff;
  58. font-size: 12px;
  59. cursor: pointer;
  60. font-family: sans-serif;
  61. opacity: 0;
  62. transition: opacity 0.2s ease-in-out;
  63. ">复制</button>`;
  64.  
  65. blocks[placeholder] = `
  66. <div style="
  67. position: relative;
  68. display: inline-block;
  69. width: 100%;
  70. background-color: #3a3f4b;
  71. border-radius: 6px;
  72. margin-bottom: 10px;
  73. overflow: hidden;
  74. " onmouseover="this.querySelector('button').style.opacity = 1"
  75. onmouseout="this.querySelector('button').style.opacity = 0">
  76. ${langLabel}
  77. ${copyButton}
  78. <pre style="
  79. background-color: #2d2d2d;
  80. color: #f8f8f2;
  81. padding: 12px;
  82. border-bottom-left-radius: 6px;
  83. border-bottom-right-radius: 6px;
  84. overflow-x: auto;
  85. font-family: 'Consolas', 'Courier New', monospace;
  86. font-size: 14px;
  87. line-height: 1.5;
  88. margin: 0;
  89. "><code>${cleanCode.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</code></pre>
  90. </div>`;
  91.  
  92. blockIndex++;
  93. return placeholder;
  94. });
  95.  
  96. // **2. 处理普通代码块**
  97. md = md.replace(/```([\s\S]*?)```/g, (match, code) => {
  98. let placeholder = `%%CODE${blockIndex}%%`;
  99. let cleanCode = code.replace(/<br\s*\/?>/g, "").trim();
  100.  
  101. let copyButton = `<button onclick="copyCode(this)" style="
  102. position: absolute;
  103. top: 6px;
  104. right: 10px;
  105. background-color: transparent;
  106. border: none;
  107. color: #ffffff;
  108. font-size: 12px;
  109. cursor: pointer;
  110. font-family: sans-serif;
  111. opacity: 0;
  112. transition: opacity 0.2s ease-in-out;
  113. ">复制</button>`;
  114.  
  115. blocks[placeholder] = `<div style="
  116. position: relative;
  117. display: inline-block;
  118. width: 100%;
  119. background-color: #3a3f4b;
  120. border-radius: 6px;
  121. margin-bottom: 10px;
  122. overflow: hidden;
  123. " onmouseover="this.querySelector('button').style.opacity = 1"
  124. onmouseout="this.querySelector('button').style.opacity = 0">
  125. ${copyButton}
  126. <pre style="
  127. background-color: #2d2d2d;
  128. color: #f8f8f2;
  129. padding: 12px;
  130. border-radius: 6px;
  131. overflow-x: auto;
  132. font-family: 'Consolas', 'Courier New', monospace;
  133. font-size: 14px;
  134. line-height: 1.5;
  135. margin: 0;
  136. "><code>${cleanCode.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</code></pre>
  137. </div>`;
  138.  
  139. blockIndex++;
  140. return placeholder;
  141. });
  142.  
  143. // **3. 还原 Markdown 形式的超链接**
  144. md = md.replace(
  145. /\[([^\]]+)\]\(<a href="([^"]+)"[^>]*>.*?<\/a>\)/g,
  146. "[$1]($2)",
  147. );
  148.  
  149. // **4. 处理标题**
  150. md = md
  151. .replace(/^### (.*$)/gm, "<h3>$1</h3>")
  152. .replace(/^## (.*$)/gm, "<h2>$1</h2>")
  153. .replace(/^# (.*$)/gm, "<h1>$1</h1>");
  154.  
  155. // **5. 处理加粗、斜体**
  156. md = md
  157. .replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
  158. .replace(/\*(.*?)\*/g, "<em>$1</em>");
  159.  
  160. // **6. 解析 Markdown 列表**
  161. md = processLists(md);
  162.  
  163. // **7. 处理行内代码**
  164. md = md.replace(
  165. /`([^`]+)`/g,
  166. `<code style="
  167. background-color: #f5f5f5;
  168. color: #d63384;
  169. padding: 2px 5px;
  170. border-radius: 4px;
  171. font-family: 'Courier New', monospace;
  172. font-size: 90%;
  173. ">$1</code>`,
  174. );
  175. // **8. 恢复代码块**
  176. Object.keys(blocks).forEach((placeholder) => {
  177. md = md.replace(placeholder, blocks[placeholder]);
  178. });
  179.  
  180. // **9. 还原 Markdown 超链接为标准 HTML `<a>`**
  181. // 还原 Markdown 超链接,支持各种协议(http, https, chrome-extension, file, mailto 等)
  182. md = md.replace(
  183. /\[([^\[\]]+)\]\(\s*(([a-zA-Z][a-zA-Z\d+\-.]*):\/\/[^\s)]+)\s*\)/g,
  184. '<a href="$2">$1</a>',
  185. );
  186.  
  187. return md;
  188. }
  189.  
  190. function processLists(md) {
  191. if (!md) return "";
  192.  
  193. let lines = md.split("\n");
  194. let output = [];
  195. let prevWasNewList = true;
  196.  
  197. lines.forEach((line) => {
  198. let isNewLine = line.trim() === "<br>";
  199. if (isNewLine) {
  200. prevWasNewList = true;
  201. output.push(line);
  202. return;
  203. }
  204.  
  205. let cleanedLine = line.replace(/<br>$/, "");
  206. let spaces = (cleanedLine.match(/^(?:&nbsp;)+/) || [""])[0].length / 6;
  207. let reducedLine = cleanedLine.replace(/^(?:&nbsp;)+/, "").trim();
  208.  
  209. // 检查有序列表 (必须是整数 + 点 + 空格)
  210. let matchOrdered = reducedLine.match(/^(\d+)\.\s+(.+)$/);
  211. // 检查无序列表 (- 或 * 后跟空格)
  212. let matchUnordered = reducedLine.match(/^([-*])\s+(.+)$/);
  213.  
  214. if (matchOrdered) {
  215. let number = matchOrdered[1];
  216. let content = matchOrdered[2];
  217. let marginLeft = spaces * 20; // 每级缩进 20px
  218. let listItem = `<div style="margin-left: ${marginLeft}px;"><span style="font-weight:bold;">${number}.</span> ${content}</div>`;
  219. output.push(listItem);
  220. prevWasNewList = false;
  221. } else if (matchUnordered) {
  222. let bullet = matchUnordered[1] === "-" ? "•" : "◦"; // 使用不同符号区分 - 和 *
  223. let content = matchUnordered[2];
  224. let marginLeft = spaces * 20;
  225. let listItem = `<div style="margin-left: ${marginLeft}px;"><span style="font-weight:bold;">${bullet}</span> ${content}</div>`;
  226. output.push(listItem);
  227. prevWasNewList = false;
  228. } else {
  229. output.push(line);
  230. prevWasNewList = false;
  231. }
  232. });
  233.  
  234. return output.join("\n");
  235. }
  236.  
  237. function processForumPosts() {
  238. document.querySelectorAll("td.t_msgfont").forEach((td) => {
  239. if (!td.dataset.processed) {
  240. let originalDiv = document.createElement("div");
  241. let markdownDiv = document.createElement("div");
  242.  
  243. originalDiv.innerHTML = td.innerHTML;
  244. markdownDiv.innerHTML = markdownToHtml(td.innerHTML);
  245.  
  246. markdownDiv.style.display = "block";
  247. originalDiv.style.display = "none";
  248.  
  249. td.innerHTML = "";
  250. td.appendChild(markdownDiv);
  251. td.appendChild(originalDiv);
  252.  
  253. td.dataset.processed = "true";
  254. td.dataset.toggled = "true"; // **默认 Markdown 模式**
  255. }
  256. });
  257. }
  258.  
  259. function toggleMarkdown(showNotification = true) {
  260. document.querySelectorAll("td.t_msgfont").forEach((td) => {
  261. if (td.dataset.processed) {
  262. let markdownDiv = td.children[0];
  263. let originalDiv = td.children[1];
  264.  
  265. if (td.dataset.toggled === "true") {
  266. markdownDiv.style.display = "none";
  267. originalDiv.style.display = "block";
  268. td.dataset.toggled = "false";
  269. if (showNotification) showToggleNotification("原始文本模式已启用");
  270. } else {
  271. markdownDiv.style.display = "block";
  272. originalDiv.style.display = "none";
  273. td.dataset.toggled = "true";
  274. if (showNotification) showToggleNotification("Markdown 模式已启用");
  275. }
  276. }
  277. });
  278. }
  279.  
  280. function showToggleNotification(message) {
  281. let notification = document.createElement("div");
  282. notification.textContent = message;
  283. notification.style.position = "fixed";
  284. notification.style.top = "10px";
  285. notification.style.left = "50%";
  286. notification.style.transform = "translateX(-50%)";
  287. notification.style.padding = "10px 20px";
  288. notification.style.backgroundColor = "black";
  289. notification.style.color = "white";
  290. notification.style.fontSize = "16px";
  291. notification.style.borderRadius = "5px";
  292. notification.style.zIndex = "1000";
  293. notification.style.opacity = "1";
  294. notification.style.transition = "opacity 1s ease-in-out";
  295. document.body.appendChild(notification);
  296.  
  297. setTimeout(() => {
  298. notification.style.opacity = "0";
  299. setTimeout(() => document.body.removeChild(notification), 1000);
  300. }, 2000);
  301. }
  302.  
  303. function setupKeyboardShortcut() {
  304. document.addEventListener("keydown", function (event) {
  305. if (event.ctrlKey && event.key === "m") {
  306. toggleMarkdown(true); // **按 Ctrl+M 时,一定要弹出通知**
  307. event.preventDefault();
  308. }
  309. });
  310. }
  311.  
  312. window.addEventListener("load", () => {
  313. processForumPosts(); // **默认 Markdown 模式**
  314. setupKeyboardShortcut();
  315. });
  316. })();

QingJ © 2025

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