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-08 提交的版本,檢視 最新版本

// ==UserScript==
// @name         4d4y Markdown Enhancer
// @namespace    http://tampermonkey.net/
// @version      2.2
// @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';

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

        let blocks = {};
        let blockIndex = 0;

        // **1. 保护 blockquote 和 blockcode**
        md = md.replace(/<(blockquote|div class="blockcode")>[\s\S]*?<\/\1>/g, (match) => {
            let placeholder = `%%BLOCK${blockIndex}%%`;
            blocks[placeholder] = match;
            blockIndex++;
            return placeholder;
        });

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

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

        // **4. 处理加粗、斜体、行内代码**
        md = md.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
               .replace(/\*(.*?)\*/g, '<em>$1</em>')
               .replace(/`(.*?)`/g, '<code>$1</code>');

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

        // **6. 恢复 blockquote、blockcode**
        Object.keys(blocks).forEach((placeholder) => {
            md = md.replace(placeholder, blocks[placeholder]);
        });

        // **7. 还原 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();
    });

})();