吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1265|回复: 23
收起左侧

[其他原创] 【油猴脚本】Markdown文件阅读器 - 纯离线/零配置/极简霓虹风

  [复制链接]
MRBANK 发表于 2026-3-16 17:48
本帖最后由 MRBANK 于 2026-4-15 16:54 编辑

它能做什么?
你是否厌倦了为了看一眼本地 .md 文件的内容,还要专门打开 VS Code 或者 Typora? 浏览器打开却是一堆乱码源码?
这款脚本就是来解决痛点的!
它能在浏览器中直接将 Markdown 源码渲染成美观的页面,完全离线运行,无需任何网络请求。
核心特性
1. 纯离线运行,隐私安全
内置解析引擎:不依赖任何第三方 CDN(如 marked.js),完全本地解析。
零网络请求[x:你的文档内容绝不会上传到任何服务器,本地文件处理更安心。

2. 智能接管,零配置
本地文件支持:直接拖拽 .md 文件到浏览器,或通过 file:// 协议打开,自动识别并渲染。
网络文件兼容:访问 URL 后缀为 .md、.markdown 或网页内容本身是纯文本源码时,自动激活。
一键切换:右上角悬浮按钮(极简霓虹风格),一键在“源码模式”与“预览模式”间切换。

3.. 极简霓虹 UI
采用 Dracula 配色方案,深色背景护眼舒适。
专为脚本设计的 "M" 霓虹图标,低调且辨识度高。
代码块语法高亮,支持 JS / Python / Java / CSS / HTML 等常见语言。

4. 完整语法支持
标题、引用、列表(有序/无序)
表格、分割线
代码块(带高亮)、行内代码
图片与链接


打开markdown文件后右上角出现切换按钮
Snipaste_2026-04-15_16-51-49.jpg
// ==UserScript==
// @name         Markdown文件阅读器
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  纯离线处理Markdown,内置解析器,完美支持本地文件,右上角一键切换预览与源码。
// @author       MRBANK
// @match        file:///*
// @match        *://*/*.md
// @match        *://*/*.markdown
// @grant        GM_addStyle
// @run-at       document-start
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij48cmVjdCB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHJ4PSI1IiBmaWxsPSIjMjgyYTM2IiBzdHJva2U9IiM1MGZhN2IiIHN0cm9rZS13aWR0aD0iMS41Ii8+PHRleHQgeD0iMTIiIHk9IjE2LjUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC13ZWlnaHQ9ImJvbGQiIGZvbnQtc2l6ZT0iMTQiIGZpbGw9IiM1MGZhN2IiPk08L3RleHQ+PC9zdmc+
// ==/UserScript==

(function() {
    'use strict';

    // ========== 配置 ==========
    const CONFIG = {
        autoRender: true,
        ignoreHosts: ['google.com', 'baidu.com', 'bing.com', 'github.com', 'twitter.com']
    };

    // ========== 内置 Markdown 解析引擎 ==========
    const MarkdownParser = {
        parse(md) {
            let html = md;
            html = this.parseCodeBlocks(html);
            html = this.escapeHtml(html);

            // Headers
            html = html.replace(/^######\s+(.+)$$$$/gm, '<h6>$$$$1</h6>');
            html = html.replace(/^#####\s+(.+)$$$$/gm, '<h5>$$$$1</h5>');
            html = html.replace(/^####\s+(.+)$$$$/gm, '<h4>$$$$1</h4>');
            html = html.replace(/^###\s+(.+)$$$$/gm, '<h3>$$$$1</h3>');
            html = html.replace(/^##\s+(.+)$$$$/gm, '<h2>$$$$1</h2>');
            html = html.replace(/^#\s+(.+)$$$$/gm, '<h1>$$$$1</h1>');

            // HR
            html = html.replace(/^(-{3,}|_{3,}|\*{3,})$$$$/gm, '<hr>');

            // Images & Links
            html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$$$$2" alt="$$$$1">');
            html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$$$$2" target="_blank">$$$$1</a>');

            // Styles
            html = html.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$$$$1</em></strong>');
            html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$$$$1</strong>');
            html = html.replace(/\*(.+?)\*/g, '<em>$$$$1</em>');
            html = html.replace(/~~(.+?)~~/g, '<del>$$$$1</del>');
            html = html.replace(/`([^`]+)`/g, '<code>$$$$1</code>');

            // Blockquotes, Lists, Tables
            html = this.parseBlockquotes(html);
            html = this.parseLists(html);
            html = this.parseTables(html);

            html = this.restoreCodeBlocks(html);
            html = this.cleanupHtmlArtifacts(html);

            // Paragraphs
            html = this.wrapParagraphs(html);

            return html;
        },

        escapeHtml(text) {
            return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
        },

        codeBlocks: [],

        parseCodeBlocks(text) {
            this.codeBlocks = [];
            return text.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => {
                const index = this.codeBlocks.push({ lang, code: code.trim() }) - 1;
                return `CODEBLOCKPLACEHOLDER$$$${index}BLOCK`;
            });
        },

        restoreCodeBlocks(text) {
            return text.replace(/CODEBLOCKPLACEHOLDER(\d+)BLOCK/g, (match, index) => {
                const block = this.codeBlocks[index];
                const escapedCode = block.code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
                return `<pre><code class="language-$$$${block.lang}">$$$${this.highlight(escapedCode, block.lang)}</code></pre>`;
            });
        },

        highlight(code, lang) {
            const keywords = {
                js: ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'import', 'export'],
                python: ['def', 'class', 'return', 'if', 'else', 'import', 'from', 'as', 'True', 'False'],
                default: []
            };
            const kwList = keywords[lang] || keywords.default;

            const placeholderStore = [];
            const stash = (text) => `HIGHLIGHTPLACEHOLDER$$$${placeholderStore.push(text) - 1}TOKEN`;

            let res = code;
            res = res.replace(/(["'`])(?:(?!\1)[^\\]|\\.)*?\1/g, match => stash(`<span class="md-hl-str">$$$${match}</span>`));
            res = res.replace(/(\/\/.*$$$$|#.*$$$$)/gm, match => stash(`<span class="md-hl-cmt">$$$${match}</span>`));
            res = res.replace(/\b(\d+\.?\d*)\b/g, '<span class="md-hl-num">$$$$1</span>');
            kwList.forEach(k => {
                res = res.replace(new RegExp(`\\b($$$${k})\\b`, 'g'), '<span class="md-hl-kw">$$$$1</span>');
            });
            res = res.replace(/HIGHLIGHTPLACEHOLDER(\d+)TOKEN/g, (match, index) => placeholderStore[Number(index)] || '');

            return res;
        },

        parseBlockquotes(text) {
            const lines = text.split('\n');
            let html = [];
            let inQuote = false;

            lines.forEach(line => {
                if (line.startsWith('> ') || line.startsWith('> ')) {
                    if (!inQuote) { html.push('<blockquote>'); inQuote = true; }
                    html.push(line.replace(/^(?:>|>)\s*/, ''));
                } else {
                    if (inQuote) { html.push('</blockquote>'); inQuote = false; }
                    html.push(line);
                }
            });
            if (inQuote) html.push('</blockquote>');
            return html.join('\n');
        },

        parseLists(text) {
            const lines = text.split('\n');
            let html = [];
            let listType = null;

            lines.forEach(line => {
                const ulMatch = line.match(/^[\s]*[-*+]\s+(.+)/);
                const olMatch = line.match(/^[\s]*\d+\.\s+(.+)/);

                if (ulMatch) {
                    if (listType !== 'ul') { if (listType) html.push(`</$$$${listType}>`); html.push('<ul>'); listType = 'ul'; }
                    html.push(`<li>$$$${ulMatch[1]}</li>`);
                } else if (olMatch) {
                    if (listType !== 'ol') { if (listType) html.push(`</$$$${listType}>`); html.push('<ol>'); listType = 'ol'; }
                    html.push(`<li>$$$${olMatch[1]}</li>`);
                } else {
                    if (listType) { html.push(`</$$$${listType}>`); listType = null; }
                    html.push(line);
                }
            });
            if (listType) html.push(`</$$$${listType}>`);
            return html.join('\n');
        },

        parseTables(text) {
            return text.replace(/^(\|.+\|)\s*\n\|[-:\s|]+\|\s*\n((?:\|.+\|\s*\n?)+)/gm, (m, h, b) => {
                const headerCells = h.split('|').slice(1, -1).map(s => `<th>$$$${s.trim()}</th>`).join('');
                const bodyRows = b.trim().split('\n').map(r => {
                    const rowCells = r.split('|').slice(1, -1).map(c => `<td>$$$${c.trim()}</td>`).join('');
                    return `<tr>$$$${rowCells}</tr>`;
                }).join('');
                return `<table><thead><tr>$$$${headerCells}</tr></thead><tbody>$$$${bodyRows}</tbody></table>`;
            });
        },

        wrapParagraphs(text) {
            const blocks = text.split(/\n\n+/);
            return blocks.map(block => {
                block = block.trim();
                if (!block) return '';
                if (/^<(h[1-6]|ul|ol|pre|blockquote|table|hr|div)\b/i.test(block)) {
                    return block;
                }
                return `$$$${block.replace(/\n/g, '<br>')}`;
            }).join('\n');
        },

        cleanupHtmlArtifacts(html) {
            return html
                .replace(/<(\/)?span class="(md-hl-(?:num|str|kw|cmt))">/g, '<$$$$1span class="$$$$2">')
                .replace(/<(\/)?(strong|em|del|code|blockquote|pre|h[1-6]|ul|ol|li|table|thead|tbody|tr|th|td|p|a|img|hr|div)([^&]*)>/g, '<$$$$1$$$$2$$$$3>')
                .replace(/<br>/g, '<br>')
                .replace(/<\/span>/g, '</span>')
                .replace(/<\/a>/g, '</a>')
                .replace(/<\/code>/g, '</code>')
                .replace(/<\/strong>/g, '</strong>')
                .replace(/<\/em>/g, '</em>')
                .replace(/<\/del>/g, '</del>')
                .replace(/"/g, '"');
        }
    };

    // ========== 样式 ==========
    GM_addStyle(`
        /* 控制面板容器 - 深色背景 */
        #md-control-panel {
            position: fixed;
            top: 12px;
            right: 12px;
            z-index: 2147483647;
            display: flex;
            background: #2b2b2b; /* 深色背景 */
            padding: 4px;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
            border: 1px solid #444;
        }

        /* 按钮通用样式 */
        .md-btn-control {
            background: transparent;
            color: #999;
            border: none;
            padding: 6px 16px;
            font-size: 14px;
            font-weight: 500;
            cursor: pointer;
            border-radius: 6px;
            transition: all 0.2s ease-in-out;
            line-height: 1.5;
        }

        /* 按钮悬浮效果 */
        .md-btn-control:hover {
            color: #fff;
        }

        /* 激活状态 - 浅灰色背景白色文字 */
        .md-btn-control.active {
            background: #555; /* 浅灰色背景 */
            color: #fff;     /* 白色文字 */
            box-shadow: 0 2px 4px rgba(0,0,0,0.2);
        }

        /* Markdown 渲染页面样式 */
        .md-rendered-body {
            background: #282a36;
            color: #f8f8f2;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
            line-height: 1.6;
            padding: 40px 20px;
            max-width: 900px;
            margin: 0 auto;
        }

        .md-rendered-body h1, .md-rendered-body h2, .md-rendered-body h3,
        .md-rendered-body h4, .md-rendered-body h5, .md-rendered-body h6 {
            color: #bd93f9;
            margin-top: 24px;
            margin-bottom: 16px;
            font-weight: 600;
            line-height: 1.25;
        }
        .md-rendered-body h1 { font-size: 2em; border-bottom: 1px solid #444; padding-bottom: .3em; }
        .md-rendered-body h2 { font-size: 1.5em; border-bottom: 1px solid #444; padding-bottom: .3em; }

        .md-rendered-body p { margin-bottom: 16px; }

        .md-rendered-body a { color: #8be9fd; text-decoration: none; }
        .md-rendered-body a:hover { text-decoration: underline; }

        .md-rendered-body code {
            background: #44475a;
            padding: 0.2em 0.4em;
            margin: 0;
            font-size: 85%;
            border-radius: 3px;
            font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
            color: #50fa7b;
        }

        .md-rendered-body pre {
            background: #1e1f29;
            border-radius: 6px;
            padding: 16px;
            overflow: auto;
            line-height: 1.45;
            font-size: 14px;
        }
        .md-rendered-body pre code {
            background: transparent;
            padding: 0;
            color: #f8f8f2;
        }

        /* 语法高亮 */
        .md-hl-kw { color: #ff79c6; }
        .md-hl-str { color: #f1fa8c; }
        .md-hl-num { color: #bd93f9; }
        .md-hl-cmt { color: #6272a4; font-style: italic; }

        .md-rendered-body blockquote {
            border-left: 4px solid #bd93f9;
            padding-left: 16px;
            margin: 0 0 16px 0;
            color: #b0b0b0;
        }

        .md-rendered-body table {
            border-collapse: collapse;
            width: 100%;
            margin-bottom: 16px;
        }
        .md-rendered-body table th, .md-rendered-body table td {
            border: 1px solid #444;
            padding: 8px 12px;
        }
        .md-rendered-body table th { background: #44475a; font-weight: 600; }
        .md-rendered-body table tr:nth-child(even) { background: rgba(40, 42, 54, 0.5); }

        .md-rendered-body img {
            max-width: 100%;
            border-radius: 4px;
            margin: 10px 0;
        }

        .md-rendered-body hr {
            border: 0;
            height: 1px;
            background-image: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0));
            margin: 2em 0;
        }
    `);

    // ========== 逻辑控制 ==========
    let isRendered = false;
    let originalContent = '';

    // 创建控制面板
    const panel = document.createElement('div');
    panel.id = 'md-control-panel';

    // 创建按钮
    const btnPreview = document.createElement('button');
    btnPreview.className = 'md-btn-control active'; // 默认激活预览
    btnPreview.textContent = 'Preview';

    const btnSource = document.createElement('button');
    btnSource.className = 'md-btn-control';
    btnSource.textContent = 'Markdown';

    const btnExportWord = document.createElement('button');
    btnExportWord.className = 'md-btn-control';
    btnExportWord.textContent = 'Export .doc';

    panel.appendChild(btnPreview);
    panel.appendChild(btnSource);
    panel.appendChild(btnExportWord);

    // 判断逻辑
    const url = window.location.href;
    const isLocalFile = window.location.protocol === 'file:';
    const isMarkdownFile = url.endsWith('.md') || url.endsWith('.markdown') || url.endsWith('.txt');
    const isPlainText = document.body ? (document.body.childElementCount === 1 && document.body.firstElementChild.tagName === 'PRE') : false;

    function init() {
        document.body.appendChild(panel);
        originalContent = document.body.innerText;

        if (isLocalFile || isMarkdownFile || isPlainText) {
            renderMarkdown();
        } else {
            // 如果不是自动渲染,默认处于源码模式,所以 Markdown 按钮应该是激活状态
            btnSource.classList.add('active');
            btnPreview.classList.remove('active');
        }
    }

    function getMarkdownSource() {
        return document.body.dataset.mdSource || originalContent || document.body.innerText || '';
    }

    function getDocumentTitle() {
        const pathname = decodeURIComponent(window.location.pathname || 'document');
        const filename = pathname.split('/').pop() || 'document';
        return filename.replace(/\.(md|markdown|txt)$$$$/i, '') || 'document';
    }

    function getExportTimestamp() {
        const now = new Date();
        const pad = (num) => String(num).padStart(2, '0');
        return `$$$${now.getFullYear()}-$$$${pad(now.getMonth() + 1)}-$$$${pad(now.getDate())} $$$${pad(now.getHours())}:$$$${pad(now.getMinutes())}`;
    }

    function escapeHtmlForWord(text) {
        return String(text || '')
            .replace(/&/g, '&')
            .replace(/</g, '<')
            .replace(/>/g, '>')
            .replace(/"/g, '"');
    }

    function buildWordHtml(title, html) {
        const exportTime = getExportTimestamp();
        return `<!DOCTYPE html>
<html xmlns:o="urn:schemas-microsoft-com:office:office"
      xmlns:w="urn:schemas-microsoft-com:office:word"
      xmlns="http://www.w3.org/TR/REC-html40">
<head>
    <meta charset="utf-8">
    <meta name="ProgId" content="Word.Document">
    <meta name="Generator" content="Markdown文件阅读器 2.0">
    <title>$$$${escapeHtmlForWord(title)}</title>
    <!--[if gte mso 9]>
    <xml>
        <w:WordDocument>
            <w:View>Print</w:View>
            <w:Zoom>100</w:Zoom>
            <w:DoNotOptimizeForBrowser/>
        </w:WordDocument>
    </xml>
    <![endif]-->
    <style>
        [url=home.php?mod=space&uid=1953840]@page[/url] {
            size: 21cm 29.7cm;
            margin: 2.2cm 1.8cm 2.2cm 1.8cm;
        }
        body {
            font-family: "Microsoft YaHei", "PingFang SC", Arial, sans-serif;
            color: #222;
            line-height: 1.75;
            margin: 0;
            font-size: 12pt;
            background: #fff;
        }
        .doc-header {
            margin-bottom: 24px;
            padding-bottom: 14px;
            border-bottom: 1px solid #d9d9d9;
        }
        .doc-title {
            font-size: 24pt;
            font-weight: 700;
            line-height: 1.3;
            color: #111;
            margin: 0 0 8px;
        }
        .doc-meta {
            font-size: 10.5pt;
            color: #666;
            margin: 0;
        }
        .doc-content {
            margin: 0;
        }
        h1, h2, h3, h4, h5, h6 {
            color: #1f1f1f;
            margin-top: 22px;
            margin-bottom: 10px;
            font-weight: 700;
            line-height: 1.4;
            page-break-after: avoid;
        }
        h1 { font-size: 20pt; border-bottom: 1px solid #d9d9d9; padding-bottom: 6px; }
        h2 { font-size: 16pt; border-bottom: 1px solid #ebebeb; padding-bottom: 4px; }
        h3 { font-size: 14pt; }
        h4 { font-size: 13pt; }
        h5, h6 { font-size: 12pt; }
        p {
            margin: 0 0 12px;
            text-align: justify;
            word-break: break-word;
        }
        a { color: #0563c1; text-decoration: underline; }
        blockquote {
            margin: 14px 0;
            padding: 8px 12px;
            border-left: 4px solid #c9c9c9;
            color: #555;
            background: #fafafa;
        }
        pre {
            white-space: pre-wrap;
            word-break: break-word;
            background: #f7f7f7;
            border: 1px solid #e0e0e0;
            border-radius: 4px;
            padding: 12px;
            margin: 12px 0 16px;
            font-family: Consolas, "Courier New", monospace;
            font-size: 10.5pt;
            line-height: 1.55;
            color: #222;
        }
        code {
            font-family: Consolas, "Courier New", monospace;
            background: #f3f3f3;
            padding: 1px 4px;
            border-radius: 3px;
            color: #222;
        }
        pre code {
            background: transparent;
            padding: 0;
            color: inherit;
        }
        table {
            border-collapse: collapse;
            width: 100%;
            margin: 12px 0 18px;
            table-layout: fixed;
        }
        th, td {
            border: 1px solid #bfbfbf;
            padding: 8px 10px;
            text-align: left;
            vertical-align: top;
            word-break: break-word;
        }
        th {
            background: #f0f0f0;
            font-weight: 700;
        }
        tr:nth-child(even) td {
            background: #fcfcfc;
        }
        img {
            max-width: 100%;
            height: auto;
            display: block;
            margin: 10px 0;
        }
        hr {
            border: none;
            border-top: 1px solid #d9d9d9;
            margin: 20px 0;
        }
        ul, ol {
            margin: 0 0 14px 26px;
            padding: 0;
        }
        li {
            margin: 0 0 6px;
        }
        li > p {
            margin-bottom: 6px;
        }
        .md-image-fallback {
            margin: 12px 0;
            padding: 8px 10px;
            border: 1px dashed #c8c8c8;
            color: #666;
            font-size: 10.5pt;
            background: #fafafa;
        }
    </style>
</head>
<body>
    <div class="doc-header">
        <div class="doc-title">$$$${escapeHtmlForWord(title)}</div>
        <p class="doc-meta">导出时间:$$$${escapeHtmlForWord(exportTime)}</p>
    </div>
    <div class="doc-content">
$$$${html}
    </div>
</body>
</html>`;
    }

    function normalizeMixedText(text) {
        return text
            .replace(/\u00a0/g, ' ')
            .replace(/[ \t]{2,}/g, ' ')
            .replace(/([\u4e00-\u9fff])\s+([A-Za-z0-9@#&])/g, '$$$$1 $$$$2')
            .replace(/([A-Za-z0-9@#&])\s+([\u4e00-\u9fff])/g, '$$$$1 $$$$2')
            .replace(/([\u4e00-\u9fff])\s+([,。!?;:、])/g, '$$$$1$$$$2')
            .replace(/([(《“‘])\s+/g, '$$$$1')
            .replace(/\s+([)》。!?;:,、”’])/g, '$$$$1')
            .replace(/\s+([,.!?;:])/g, '$$$$1')
            .replace(/([,.!?;:])(?!\s|$$$$|[)\]}>"'])/g, '$$$$1 ')
            .replace(/\(\s+/g, '(')
            .replace(/\s+\)/g, ')')
            .replace(/\[\s+/g, '[')
            .replace(/\s+\]/g, ']')
            .replace(/\{\s+/g, '{')
            .replace(/\s+\}/g, '}')
            .trim();
    }

    function normalizeBlockHtml(html) {
        return html
            .replace(/ /g, ' ')
            .replace(/\s*<br\s*\/?>\s*/gi, '<br>')
            .replace(/(<br>){3,}/gi, '<br><br>')
            .replace(/[ \t]{2,}/g, ' ')
            .replace(/(?:<br>\s*)+$$$$/gi, '')
            .replace(/^(?:\s*<br>)+/gi, '')
            .trim();
    }

    function simplifyWordHtml(html) {
        const container = document.createElement('div');
        container.innerHTML = html;

        container.querySelectorAll('pre code').forEach(codeEl => {
            const plainCode = codeEl.textContent || '';
            const newCode = document.createElement('code');
            newCode.textContent = plainCode
                .replace(/\r\n/g, '\n')
                .replace(/\n{3,}/g, '\n\n')
                .trimEnd();
            codeEl.replaceWith(newCode);
        });

        container.querySelectorAll('code:not(pre code)').forEach(codeEl => {
            codeEl.textContent = (codeEl.textContent || '').replace(/\s+/g, ' ').trim();
        });

        container.querySelectorAll('br + br').forEach(br => br.remove());

        container.querySelectorAll('p, li, blockquote, td, th').forEach(el => {
            el.innerHTML = normalizeBlockHtml(el.innerHTML);
        });

        container.querySelectorAll('p, li, blockquote, td, th, h1, h2, h3, h4, h5, h6').forEach(el => {
            if (el.children.length === 0) {
                el.textContent = normalizeMixedText(el.textContent || '');
            }
        });

        container.querySelectorAll('li').forEach(li => {
            li.innerHTML = normalizeBlockHtml(li.innerHTML);
            if (!li.innerHTML) {
                li.remove();
            }
        });

        container.querySelectorAll('ul, ol').forEach(list => {
            const items = list.querySelectorAll(':scope > li');
            if (items.length === 0) {
                list.remove();
            }
        });

        container.querySelectorAll('blockquote').forEach(quote => {
            quote.innerHTML = normalizeBlockHtml(quote.innerHTML);
        });

        container.querySelectorAll('td, th').forEach(cell => {
            cell.innerHTML = normalizeBlockHtml(cell.innerHTML);
        });

        container.querySelectorAll('p').forEach(p => {
            if (!p.textContent.trim() && !p.querySelector('img, br, code')) {
                p.remove();
            }
        });

        container.querySelectorAll('img').forEach(img => {
            const src = img.getAttribute('src') || '';
            const alt = normalizeMixedText(img.getAttribute('alt') || '图片');
            const fallback = document.createElement('div');
            fallback.className = 'md-image-fallback';
            fallback.innerHTML = src
                ? `图片:$$$${escapeHtmlForWord(alt)}<br>路径:$$$${escapeHtmlForWord(src)}`
                : `图片:$$$${escapeHtmlForWord(alt)}`;
            img.replaceWith(fallback);
        });

        container.querySelectorAll('*').forEach(el => {
            Array.from(el.attributes).forEach(attr => {
                if (attr.name === 'class' || attr.name === 'style') {
                    el.removeAttribute(attr.name);
                }
            });
        });

        let cleanedHtml = container.innerHTML;
        cleanedHtml = cleanedHtml
            .replace(/&nbsp;/g, ' ')
            .replace(/ /g, ' ')
            .replace(/\u00a0/g, ' ')
            .replace(/\n{3,}/g, '\n\n')
            .replace(/>\s+</g, '><')
            .replace(/<(p|li|blockquote|td|th)><br><\/(p|li|blockquote|td|th)>/gi, '')
            .replace(/<(ul|ol)>\s*<\/(ul|ol)>/gi, '')
            .trim();

        cleanedHtml = cleanedHtml
            .replace(/\s*<\/p>/gi, '')
            .replace(/<li>\s*<\/li>/gi, '')
            .replace(/<blockquote>\s*<\/blockquote>/gi, '')
            .replace(/<td>\s*<\/td>/gi, '<td></td>')
            .replace(/<th>\s*<\/th>/gi, '<th></th>')
            .replace(/<br>\s*<\/li>/gi, '</li>')
            .replace(/<br>\s*<\/p>/gi, '')
            .replace(/<br>\s*<\/blockquote>/gi, '</blockquote>')
            .replace(/<\/li>\s*<li>/gi, '</li><li>')
            .replace(/<\/p>\s*/gi, '<p>')
            .replace(/<\/blockquote>\s*<blockquote>/gi, '<br>');

        return cleanedHtml;
    }

    function downloadBlob(filename, content, mimeType, addBom = true) {
        const blob = content instanceof Blob
            ? content
            : new Blob(addBom ? ['\ufeff', content] : [content], { type: mimeType });
        const objectUrl = URL.createObjectURL(blob);
        const link = document.createElement('a');
        link.href = objectUrl;
        link.download = filename;
        document.body.appendChild(link);
        link.click();
        link.remove();
        URL.revokeObjectURL(objectUrl);
    }

    function exportWord() {
        const source = getMarkdownSource();
        const html = MarkdownParser.parse(source);
        const simplifiedHtml = simplifyWordHtml(html);
        const title = getDocumentTitle();
        const wordHtml = buildWordHtml(title, simplifiedHtml);
        downloadBlob(`$$$${title}.doc`, wordHtml, 'application/msword;charset=utf-8');
    }


    function renderMarkdown() {
        if (isRendered) return;

        if (!document.body.dataset.mdSource) {
            document.body.dataset.mdSource = originalContent;
        }

        const html = MarkdownParser.parse(document.body.dataset.mdSource);

        document.body.innerHTML = '';
        document.body.className = 'md-rendered-body';
        document.body.innerHTML = html;

        document.body.appendChild(panel);

        // 更新按钮状态
        btnPreview.classList.add('active');
        btnSource.classList.remove('active');

        isRendered = true;
    }

    function showSource() {
        if (!isRendered) return;

        document.body.className = '';
        document.body.innerHTML = '';

        const pre = document.createElement('pre');
        pre.style.whiteSpace = 'pre-wrap';
        pre.style.wordWrap = 'break-word';
        pre.style.fontFamily = 'monospace';
        pre.textContent = document.body.dataset.mdSource || originalContent;
        document.body.appendChild(pre);

        document.body.appendChild(panel);

        // 更新按钮状态
        btnSource.classList.add('active');
        btnPreview.classList.remove('active');

        isRendered = false;
    }

    // 绑定事件
    btnPreview.addEventListener('click', () => {
        if (!isRendered) renderMarkdown();
    });

    btnSource.addEventListener('click', () => {
        if (isRendered) showSource();
    });

    btnExportWord.addEventListener('click', () => {
        try {
            exportWord();
        } catch (error) {
            console.error('导出 Word 失败:', error);
            alert('导出 Word 失败,请打开控制台查看错误信息。');
        }
    });

    // 等待DOM加载
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();
Markdown文件阅读器-2.0.user.js.zip (12.29 KB, 下载次数: 50, 售价: 3 CB吾爱币)

免费评分

参与人数 6吾爱币 +12 热心值 +6 收起 理由
苏紫方璇 + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
Codeman + 1 + 1 我很赞同!
moyuan0115 + 1 + 1 我很赞同!
yks1985 + 1 + 1 谢谢@Thanks!
q2825q + 1 + 1 热心回复!
p9527 + 1 + 1 谢谢分享

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

不知道改成啥 发表于 2026-3-17 10:04
image.png image.png
我测试了下这个装上打开md文件还是这样是哪里设置有问题吗?
Doublevv 发表于 2026-3-19 10:18
脚本不生效的解决:
到扩展程序管理页面,点油猴/篡改猴/脚本猫 的 “详情”,打开 允许访问文件网址 权限,这样脚本才能生效。
czwuyang 发表于 2026-3-17 06:10
这工具 简洁明了,免得为了看一眼MD文档,要用重量的软件。谢谢
phoebusor 发表于 2026-3-17 07:58
好东西,感谢分享!!!!!!
hlowld 发表于 2026-3-17 08:17
支持原创,之前一直用markdown preview plus,看下这个效果如何
zy75974350 发表于 2026-3-17 09:31
好东西,已经收藏了,感谢分享
yks1985 发表于 2026-3-17 10:07
好东西,下载收藏分享
 楼主| MRBANK 发表于 2026-3-17 10:19
不知道改成啥 发表于 2026-3-17 10:04
我测试了下这个装上打开md文件还是这样是哪里设置有问题吗?

下载附件js文件吧,粘贴的代码顶部格式出问题了。已经重新提交,待审核
moyuan0115 发表于 2026-3-17 12:50
感谢分享
tgf52 发表于 2026-3-17 13:25
感谢分享,拿走试用一下
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-4-17 16:13

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表