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.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
  });
})();