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.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         4d4y Markdown Enhancer
// @namespace    http://tampermonkey.net/
// @version      2.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.
// @match        https://www.4d4y.com/forum/*
// @author       屋大维 + ChatGPT
// @license      MIT
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function () {
  "use strict";

  window.copyCode = function (button) {
    let codeElement = button.parentElement.querySelector("pre code");
    if (!codeElement) return;

    let text = codeElement.innerText;
    navigator.clipboard.writeText(text).then(() => {
      button.innerText = "已复制!";
      setTimeout(() => (button.innerText = "复制"), 1500);
    });
  };

  function markdownToHtml(md) {
    if (!md) return "";

    let blocks = {};
    let blockIndex = 0;

    // **1. 处理带语言标签的代码块**
    md = md.replace(/```(\w+)\s*<br>\s*([\s\S]*?)```/g, (match, lang, code) => {
      let placeholder = `%%CODE${blockIndex}%%`;
      let cleanCode = code.replace(/<br\s*\/?>/g, "").trim();

      let langLabel = `<div style="
            background-color: #3a3f4b;
            color: #ffffff;
            font-size: 12px;
            font-weight: bold;
            padding: 6px 12px;
            border-top-left-radius: 6px;
            font-family: sans-serif;
            display: inline-block;
            min-width: 100px;
            text-align: left;
        ">${lang}</div>`;

      let copyButton = `<button onclick="copyCode(this)" style="
            position: absolute;
            top: 6px;
            right: 10px;
            background-color: transparent;
            border: none;
            color: #ffffff;
            font-size: 12px;
            cursor: pointer;
            font-family: sans-serif;
            opacity: 0;
            transition: opacity 0.2s ease-in-out;
        ">复制</button>`;

      blocks[placeholder] = `
        <div style="
            position: relative;
            display: inline-block;
            width: 100%;
            background-color: #3a3f4b;
            border-radius: 6px;
            margin-bottom: 10px;
            overflow: hidden;
        " onmouseover="this.querySelector('button').style.opacity = 1"
          onmouseout="this.querySelector('button').style.opacity = 0">
            ${langLabel}
            ${copyButton}
            <pre style="
                background-color: #2d2d2d;
                color: #f8f8f2;
                padding: 12px;
                border-bottom-left-radius: 6px;
                border-bottom-right-radius: 6px;
                overflow-x: auto;
                font-family: 'Consolas', 'Courier New', monospace;
                font-size: 14px;
                line-height: 1.5;
                margin: 0;
            "><code>${cleanCode.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</code></pre>
        </div>`;

      blockIndex++;
      return placeholder;
    });

    // **2. 处理普通代码块**
    md = md.replace(/```([\s\S]*?)```/g, (match, code) => {
      let placeholder = `%%CODE${blockIndex}%%`;
      let cleanCode = code.replace(/<br\s*\/?>/g, "").trim();

      let copyButton = `<button onclick="copyCode(this)" style="
            position: absolute;
            top: 6px;
            right: 10px;
            background-color: transparent;
            border: none;
            color: #ffffff;
            font-size: 12px;
            cursor: pointer;
            font-family: sans-serif;
            opacity: 0;
            transition: opacity 0.2s ease-in-out;
        ">复制</button>`;

      blocks[placeholder] = `<div style="
            position: relative;
            display: inline-block;
            width: 100%;
            background-color: #3a3f4b;
            border-radius: 6px;
            margin-bottom: 10px;
            overflow: hidden;
        " onmouseover="this.querySelector('button').style.opacity = 1"
          onmouseout="this.querySelector('button').style.opacity = 0">
            ${copyButton}
            <pre style="
                background-color: #2d2d2d;
                color: #f8f8f2;
                padding: 12px;
                border-radius: 6px;
                overflow-x: auto;
                font-family: 'Consolas', 'Courier New', monospace;
                font-size: 14px;
                line-height: 1.5;
                margin: 0;
            "><code>${cleanCode.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</code></pre>
        </div>`;

      blockIndex++;
      return placeholder;
    });

    // **3. 还原 Markdown 形式的超链接**
    md = md.replace(
      /\[([^\]]+)\]\(<a href="([^"]+)"[^>]*>.*?<\/a>\)/g,
      "[$1]($2)",
    );

    // **4. 处理标题**
    md = md
      .replace(/^### (.*$)/gm, "<h3>$1</h3>")
      .replace(/^## (.*$)/gm, "<h2>$1</h2>")
      .replace(/^# (.*$)/gm, "<h1>$1</h1>");

    // **5. 处理加粗、斜体**
    md = md
      .replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
      .replace(/\*(.*?)\*/g, "<em>$1</em>");

    // **6. 解析 Markdown 列表**
    md = processLists(md);

    // **7. 处理行内代码**
    md = md.replace(
      /`([^`]+)`/g,
      `<code style="
        background-color: #f5f5f5;
        color: #d63384;
        padding: 2px 5px;
        border-radius: 4px;
        font-family: 'Courier New', monospace;
        font-size: 90%;
    ">$1</code>`,
    );
    // **8. 恢复代码块**
    Object.keys(blocks).forEach((placeholder) => {
      md = md.replace(placeholder, blocks[placeholder]);
    });

    // **9. 还原 Markdown 超链接为标准 HTML `<a>`**
    // 还原 Markdown 超链接,支持各种协议(http, https, chrome-extension, file, mailto 等)
    md = md.replace(
      /\[([^\[\]]+)\]\(\s*(([a-zA-Z][a-zA-Z\d+\-.]*):\/\/[^\s)]+)\s*\)/g,
      '<a href="$2">$1</a>',
    );

    return md;
  }

  function processLists(md) {
    if (!md) return "";

    let lines = md.split("\n");
    let output = [];
    let prevWasNewList = true;

    lines.forEach((line) => {
      let isNewLine = line.trim() === "<br>";
      if (isNewLine) {
        prevWasNewList = true;
        output.push(line);
        return;
      }

      let cleanedLine = line.replace(/<br>$/, "");
      let spaces = (cleanedLine.match(/^(?:&nbsp;)+/) || [""])[0].length / 6;
      let reducedLine = cleanedLine.replace(/^(?:&nbsp;)+/, "").trim();

      // 检查有序列表 (必须是整数 + 点 + 空格)
      let matchOrdered = reducedLine.match(/^(\d+)\.\s+(.+)$/);
      // 检查无序列表 (- 或 * 后跟空格)
      let matchUnordered = reducedLine.match(/^([-*])\s+(.+)$/);

      if (matchOrdered) {
        let number = matchOrdered[1];
        let content = matchOrdered[2];
        let marginLeft = spaces * 20; // 每级缩进 20px
        let listItem = `<div style="margin-left: ${marginLeft}px;"><span style="font-weight:bold;">${number}.</span> ${content}</div>`;
        output.push(listItem);
        prevWasNewList = false;
      } else if (matchUnordered) {
        let bullet = matchUnordered[1] === "-" ? "•" : "◦"; // 使用不同符号区分 - 和 *
        let content = matchUnordered[2];
        let marginLeft = spaces * 20;
        let listItem = `<div style="margin-left: ${marginLeft}px;"><span style="font-weight:bold;">${bullet}</span> ${content}</div>`;
        output.push(listItem);
        prevWasNewList = false;
      } else {
        output.push(line);
        prevWasNewList = false;
      }
    });

    return output.join("\n");
  }

  function processForumPosts() {
    document.querySelectorAll("td.t_msgfont").forEach((td) => {
      if (!td.dataset.processed) {
        let originalDiv = document.createElement("div");
        let markdownDiv = document.createElement("div");

        originalDiv.innerHTML = td.innerHTML;
        markdownDiv.innerHTML = markdownToHtml(td.innerHTML);

        markdownDiv.style.display = "block";
        originalDiv.style.display = "none";

        td.innerHTML = "";
        td.appendChild(markdownDiv);
        td.appendChild(originalDiv);

        td.dataset.processed = "true";
        td.dataset.toggled = "true"; // **默认 Markdown 模式**
      }
    });
  }

  function toggleMarkdown(showNotification = true) {
    document.querySelectorAll("td.t_msgfont").forEach((td) => {
      if (td.dataset.processed) {
        let markdownDiv = td.children[0];
        let originalDiv = td.children[1];

        if (td.dataset.toggled === "true") {
          markdownDiv.style.display = "none";
          originalDiv.style.display = "block";
          td.dataset.toggled = "false";
          if (showNotification) showToggleNotification("原始文本模式已启用");
        } else {
          markdownDiv.style.display = "block";
          originalDiv.style.display = "none";
          td.dataset.toggled = "true";
          if (showNotification) showToggleNotification("Markdown 模式已启用");
        }
      }
    });
  }

  function showToggleNotification(message) {
    let notification = document.createElement("div");
    notification.textContent = message;
    notification.style.position = "fixed";
    notification.style.top = "10px";
    notification.style.left = "50%";
    notification.style.transform = "translateX(-50%)";
    notification.style.padding = "10px 20px";
    notification.style.backgroundColor = "black";
    notification.style.color = "white";
    notification.style.fontSize = "16px";
    notification.style.borderRadius = "5px";
    notification.style.zIndex = "1000";
    notification.style.opacity = "1";
    notification.style.transition = "opacity 1s ease-in-out";
    document.body.appendChild(notification);

    setTimeout(() => {
      notification.style.opacity = "0";
      setTimeout(() => document.body.removeChild(notification), 1000);
    }, 2000);
  }

  function setupKeyboardShortcut() {
    document.addEventListener("keydown", function (event) {
      if (event.ctrlKey && event.key === "m") {
        toggleMarkdown(true); // **按 Ctrl+M 时,一定要弹出通知**
        event.preventDefault();
      }
    });
  }

  window.addEventListener("load", () => {
    processForumPosts(); // **默认 Markdown 模式**
    setupKeyboardShortcut();
  });
})();