Greasy Fork is available in English.

TGFC Markdown Enhancer

TGFC论坛贴内 Markdown 美化,优化阅读边距,调整标题配色,保持紧凑排版,仅针对楼层内容

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

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

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         TGFC Markdown Enhancer
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  TGFC论坛贴内 Markdown 美化,优化阅读边距,调整标题配色,保持紧凑排版,仅针对楼层内容
// @author       heiren + ChatGPT
// @match        https://s.tgfcer.com/*
// @match        https://tgfcer.com/*
// @icon         https://s.tgfcer.com/favicon.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    class TGMarkdownEnhancer {
        constructor() {
            this.isEnabled = GM_getValue ? GM_getValue('tgmd_enabled', false) : false;
            // 限制仅在楼层正文生效,防止误伤签名档
            this.selectors = [
                'div.postmessage.defaultpost'
            ];
            this.originalHtmlMap = new Map();
            this.processedNodes = new WeakSet();
            this.observer = null;
            this.modeBtn = null;

            this.init();
        }

        init() {
            this.injectStyles();
            this.createButton();
            this.bindEvents();

            if (this.isEnabled) {
                this.startEnhancement();
            }
        }

        bindEvents() {
            // 快捷键: Ctrl/Meta + Alt + V
            window.addEventListener('keydown', (e) => {
                const tag = (e.target && e.target.tagName) ? e.target.tagName.toLowerCase() : '';
                const ignore = ['input', 'textarea', 'select'].includes(tag) || e.target.isContentEditable;
                if (ignore) return;

                if ((e.metaKey || e.ctrlKey) && e.altKey && e.key.toLowerCase() === 'v') {
                    this.toggleState();
                }
            });

            // 复制代码按钮点击事件
            document.body.addEventListener('click', (e) => {
                const btn = e.target.closest('.tgfc-md-copy');
                if (btn) {
                    this.handleCopy(btn);
                }
            });
        }

        toggleState() {
            this.isEnabled = !this.isEnabled;
            if (GM_setValue) GM_setValue('tgmd_enabled', this.isEnabled);
            this.updateBtnUI();

            if (this.isEnabled) {
                this.startEnhancement();
                this.showNotification('TGMD: 已开启');
            } else {
                this.stopEnhancement();
                this.showNotification('TGMD: 已关闭');
            }
        }

        startEnhancement() {
            this.processExistingNodes();
            this.startObserver();
        }

        stopEnhancement() {
            this.stopObserver();
            this.restoreOriginalContent();
        }

        startObserver() {
            if (this.observer) return;
            this.observer = new MutationObserver((mutations) => {
                let shouldProcess = false;
                for (const mutation of mutations) {
                    if (mutation.addedNodes.length > 0) {
                        shouldProcess = true;
                        break;
                    }
                }
                if (shouldProcess) {
                    this.processExistingNodes();
                }
            });
            this.observer.observe(document.body, { childList: true, subtree: true });
        }

        stopObserver() {
            if (this.observer) {
                this.observer.disconnect();
                this.observer = null;
            }
        }

        collectTargets() {
            let nodes = [];
            for (let sel of this.selectors) {
                let found = document.querySelectorAll(sel);
                if (found.length > 0) {
                    nodes = nodes.concat(Array.from(found));
                }
            }
            return [...new Set(nodes)];
        }

        processExistingNodes() {
            const targets = this.collectTargets();
            targets.forEach(node => {
                if (this.processedNodes.has(node)) return;

                if (!this.originalHtmlMap.has(node)) {
                    this.originalHtmlMap.set(node, node.innerHTML);
                }

                this.enhanceNode(node);
            });
        }

        restoreOriginalContent() {
            const targets = this.collectTargets();
            targets.forEach(node => {
                if (this.originalHtmlMap.has(node)) {
                    node.innerHTML = this.originalHtmlMap.get(node);
                    this.processedNodes.delete(node);
                }
            });
        }

        enhanceNode(node) {
            let postInfo = node.querySelector('.postinfo');
            let contentHtml = '', contentText = '';

            // 提取内容时排除 postinfo
            if (postInfo) {
                let clone = node.cloneNode(true);
                let piInClone = clone.querySelector('.postinfo');
                if (piInClone) piInClone.remove();
                contentHtml = clone.innerHTML;
                contentText = clone.innerText || clone.textContent;
            } else {
                contentHtml = node.innerHTML;
                contentText = node.innerText || node.textContent;
            }

            if (this.shouldEnhance(contentHtml, contentText)) {
                let raw = this.cleanRawText(contentText);
                let newContentHtml = this.markdownToHtml(raw);

                node.innerHTML = '';
                if (postInfo) node.appendChild(postInfo);
                node.insertAdjacentHTML('beforeend', newContentHtml);

                this.processedNodes.add(node);
            }
        }

        shouldEnhance(html, text) {
            // 避免重复渲染或破坏已有格式
            if (/<blockquote|div class="quote"|div class="t_msgfont"|table/i.test(html)) {
                if (/quote/i.test(html)) return false;
            }
            const hasCodeBlock = /```[\s\S]*?```/.test(text);
            const hasHeader = /^#{1,6}\s+/m.test(text);
            return hasCodeBlock || hasHeader;
        }

        cleanRawText(text) {
            if (!text) return '';
            text = text.replace(/\(adsbygoogle\s*=\s*window\.adsbygoogle\s*\|\|\s*\[\]\)\.push\(\{\}\);/g, '');
            return text.trim();
        }

        markdownToHtml(md) {
            if (!md) return '';
            md = this.cleanRawText(md);

            // 1. 代码块
            md = md.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => {
                let langAttr = lang ? `data-lang="${lang}"` : '';
                return `<pre class="tgfc-md-code" ${langAttr}><code>${this.escapeHtml(code.trim())}</code><button class="tgfc-md-copy">复制</button></pre>`;
            });

            // 2. 行内代码
            md = md.replace(/`([^`]+)`/g, '<code class="tgfc-inline">$1</code>');

            // 3. 标题
            const headers = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
            headers.forEach((tag, i) => {
                const level = i + 1;
                const regex = new RegExp(`^#{${level}}\\s+(.+)$`, 'gm');
                md = md.replace(regex, `<${tag}>$1</${tag}>`);
            });

            // 4. 水平线
            md = md.replace(/^---$/gm, '<hr class="tgfc-md-hr">');

            // 5. 图片
            md = md.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" class="tgfc-md-img">');

            // 6. 链接
            md = md.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');

            // 7. 样式 (粗体、斜体、引用)
            md = md.replace(/\*\*([^*]+)\*\*/g, '<span class="tgfc-md-bold">$1</span>');
            md = md.replace(/\*([^*]+)\*/g, '<span class="tgfc-md-italic">$1</span>');
            md = md.replace(/^> ?(.+)$/gm, '<blockquote>$1</blockquote>');

            // 8. 列表项
            md = md.replace(/^\s*\d+\.\s+(.+)$/gm, '<li class="tgfc-md-li" style="list-style:decimal;">$1</li>');
            md = md.replace(/^\s*[-*+] +(.+)$/gm, '<li class="tgfc-md-li">$1</li>');

            // 9. 换行
            md = md.replace(/(?<!>)\n/g, '<br>');

            // 10. 列表包裹
            md = md.replace(/(<li class="tgfc-md-li" style="list-style:decimal;">[\s\S]*?<\/li>)+/gm, '<ol class="tgfc-md-ol">$&</ol>');
            md = md.replace(/(<li class="tgfc-md-li">[\s\S]*?<\/li>)+/gm, '<ul class="tgfc-md-ul">$&</ul>');

            return `<div class="tgfc-md-content">${md}</div>`;
        }

        escapeHtml(str) {
            return str.replace(/[&<>"]/g, (tag) => {
                const charsToReplace = {
                    '&': '&amp;',
                    '<': '&lt;',
                    '>': '&gt;',
                    '"': '&quot;'
                };
                return charsToReplace[tag] || tag;
            });
        }

        handleCopy(btn) {
            let codeEl = btn.previousElementSibling;
            if (codeEl) {
                let txt = codeEl.textContent;
                navigator.clipboard.writeText(txt).then(() => {
                    const originalText = btn.innerText;
                    btn.innerText = '已复制!';
                    setTimeout(() => btn.innerText = originalText, 1200);
                });
            }
        }

        createButton() {
            this.modeBtn = document.createElement('button');
            this.modeBtn.style.cssText = `
                position: fixed;
                top: 20px;
                left: 22px;
                z-index: 10001;
                padding: 6px 12px;
                border-radius: 6px;
                color: white;
                border: none;
                font-size: 12px;
                line-height: 1.2;
                box-shadow: 0 2px 5px rgba(0,0,0,0.2);
                cursor: pointer;
                opacity: 0.8;
                transition: all 0.2s;
                font-family: system-ui, -apple-system, sans-serif;
            `;
            this.modeBtn.title = "点击切换:开启 / 关闭";

            this.modeBtn.addEventListener('mouseenter', () => this.modeBtn.style.opacity = '1');
            this.modeBtn.addEventListener('mouseleave', () => this.modeBtn.style.opacity = '0.8');
            this.modeBtn.onclick = () => this.toggleState();

            this.updateBtnUI();
            document.body.appendChild(this.modeBtn);
        }

        updateBtnUI() {
            if (this.isEnabled) {
                this.modeBtn.innerText = 'TGMD: ON';
                this.modeBtn.style.background = '#2879ff';
            } else {
                this.modeBtn.innerText = 'TGMD: OFF';
                this.modeBtn.style.background = '#718096';
            }
        }

        showNotification(msg) {
            let notif = document.createElement('div');
            notif.innerText = msg;
            notif.style.cssText = `
                position: fixed;
                top: 38px;
                right: 36px;
                background: rgba(0,0,0,0.8);
                color: #fff;
                padding: 8px 16px;
                border-radius: 6px;
                z-index: 9999;
                font-size: 13px;
                box-shadow: 0 4px 12px rgba(0,0,0,0.15);
                animation: fadeIn 0.2s;
                backdrop-filter: blur(4px);
            `;
            document.body.appendChild(notif);
            setTimeout(() => notif.remove(), 1500);
        }

        injectStyles() {
            const style = `
                /* --- 基础容器 --- */
                .tgfc-md-content {
                    font-size: 15px !important;
                    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
                    color: #24292e;
                    background: #ffffff !important;
                    border-radius: 8px;
                    padding: 15px 20px;
                    margin: 12px auto;
                    max-width: 96%;
                    line-height: 1.45;
                    border: 1px solid #e1e4e8;
                    box-shadow: 0 1px 3px rgba(0,0,0,0.04);
                }

                /* --- 标题通用设置 --- */
                .tgfc-md-content h1, .tgfc-md-content h2, .tgfc-md-content h3,
                .tgfc-md-content h4, .tgfc-md-content h5, .tgfc-md-content h6 {
                    font-family: inherit;
                    font-weight: 700;
                    margin-top: 10px;
                    margin-bottom: 6px;
                    line-height: 1.3;
                    padding: 5px 10px;
                    border-radius: 4px;
                    border-left-style: solid;
                    color: #24292e;
                }

                /* H1: 中宝蓝 */
                .tgfc-md-content h1 {
                    background: #2b6cb0;
                    color: #ffffff;
                    border-left-width: 6px;
                    border-left-color: #1a365d;
                    font-size: 1.3em;
                    border-bottom: none;
                }

                /* H2: 亮蓝 */
                .tgfc-md-content h2 {
                    background: #4299e1;
                    color: #ffffff;
                    border-left-width: 5px;
                    border-left-color: #2b6cb0;
                    font-size: 1.4em;
                    border-bottom: none;
                }

                /* H3: 浅蓝背景 + 深蓝字 */
                .tgfc-md-content h3 {
                    background: #ebf8ff;
                    color: #2b6cb0;
                    border-left-width: 4px;
                    border-left-color: #4299e1;
                    font-size: 1.3em;
                }

                /* H4: 极淡绿背景 */
                .tgfc-md-content h4 {
                    background: #f0fff4;
                    color: #2f855a;
                    border-left-width: 4px;
                    border-left-color: #48bb78;
                    font-size: 1.2em;
                }

                /* H5: 浅灰 */
                .tgfc-md-content h5 {
                    background: #f7fafc;
                    color: #4a5568;
                    border-left-width: 3px;
                    border-left-color: #cbd5e0;
                    font-size: 1.1em;
                }

                /* H6 */
                .tgfc-md-content h6 {
                    background: #fff;
                    border: 1px solid #eee;
                    border-left-width: 3px;
                    border-left-color: #e2e8f0;
                    font-size: 1.0em;
                    color: #718096;
                }

                /* --- 内容元素 --- */
                .tgfc-md-content p { margin-bottom: 4px; }
                
                .tgfc-md-content blockquote {
                    border-left: 4px solid #dfe2e5;
                    margin: 0 0 6px 0;
                    padding: 0 1em;
                    color: #6a737d;
                }

                .tgfc-md-content ul, .tgfc-md-content ol {
                    padding-left: 2em;
                    margin-bottom: 6px;
                }
                
                .tgfc-md-content li { margin-bottom: 4px; }
                
                .tgfc-md-content hr.tgfc-md-hr {
                    height: 0.25em;
                    padding: 0;
                    margin: 24px 0;
                    background-color: #e1e4e8;
                    border: 0;
                }

                .tgfc-md-content img.tgfc-md-img {
                    max-width: 100%;
                    box-sizing: content-box;
                    background-color: #fff;
                    border-radius: 4px;
                    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
                }

                /* --- 代码块 --- */
                .tgfc-md-code {
                    position: relative;
                    background: #f6f8fa;
                    border-radius: 6px;
                    font-size: 85%;
                    line-height: 1.45;
                    padding: 16px;
                    margin-bottom: 16px;
                    overflow: auto;
                    font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
                }
                
                .tgfc-md-code .tgfc-md-copy {
                    position: absolute;
                    top: 8px;
                    right: 8px;
                    font-size: 12px;
                    background: rgba(255,255,255,0.8);
                    color: #24292e;
                    border: 1px solid rgba(27,31,35,0.15);
                    border-radius: 4px;
                    padding: 3px 8px;
                    cursor: pointer;
                    transition: all 0.2s;
                    opacity: 0;
                }
                
                .tgfc-md-code:hover .tgfc-md-copy { opacity: 1; }
                .tgfc-md-code .tgfc-md-copy:hover { background: #fff; }

                .tgfc-inline {
                    padding: 0.2em 0.4em;
                    margin: 0;
                    font-size: 85%;
                    background-color: rgba(27,31,35,0.05);
                    border-radius: 3px;
                    font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
                }

                /* --- 链接与强调 --- */
                .tgfc-md-content a { color: #0366d6; text-decoration: none; }
                .tgfc-md-content a:hover { text-decoration: underline; }
                .tgfc-md-bold { font-weight: 600; }
                .tgfc-md-italic { font-style: italic; }
                
                /* 动画 */
                @keyframes fadeIn {
                    from { opacity: 0; transform: translateY(-10px); }
                    to { opacity: 1; transform: translateY(0); }
                }
            `;

            let styleEl = document.createElement('style');
            styleEl.innerText = style;
            document.head.appendChild(styleEl);
        }
    }

    // Initialize
    new TGMarkdownEnhancer();

})();