// ==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, '
$1
'); html = html.replace(/^#####\s+(.+)$/gm, '
$1
'); html = html.replace(/^####\s+(.+)$/gm, '

$1

'); html = html.replace(/^###\s+(.+)$/gm, '

$1

'); html = html.replace(/^##\s+(.+)$/gm, '

$1

'); html = html.replace(/^#\s+(.+)$/gm, '

$1

'); // HR html = html.replace(/^(-{3,}|_{3,}|\*{3,})$/gm, '
'); // Images & Links html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1'); html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); // Styles html = html.replace(/\*\*\*(.+?)\*\*\*/g, '$1'); html = html.replace(/\*\*(.+?)\*\*/g, '$1'); html = html.replace(/\*(.+?)\*/g, '$1'); html = html.replace(/~~(.+?)~~/g, '$1'); html = html.replace(/`([^`]+)`/g, '$1'); // 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, '>'); }, 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, '>'); return `
${this.highlight(escapedCode, block.lang)}
`; }); }, 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(`${match}`)); res = res.replace(/(\/\/.*$|#.*$)/gm, match => stash(`${match}`)); res = res.replace(/\b(\d+\.?\d*)\b/g, '$1'); kwList.forEach(k => { res = res.replace(new RegExp(`\\b(${k})\\b`, 'g'), '$1'); }); 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('
'); inQuote = true; } html.push(line.replace(/^(?:>|>)\s*/, '')); } else { if (inQuote) { html.push('
'); inQuote = false; } html.push(line); } }); if (inQuote) html.push(''); 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(``); html.push('