/* * marked.js —— 轻量 Markdown 解析器(本地自带,不依赖任何 CDN) * 用法:marked.parse(markdownString) -> htmlString * 支持:标题、粗体/斜体/删除线、行内代码、代码块、引用、有序/无序列表(含嵌套)、 * 链接、图片、分隔线、GFM 表格、段落与软换行。 * 安全:先转义原文 HTML,链接 URL 做白名单过滤,避免 XSS。 */ (function (global) { 'use strict'; function escapeHtml(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>'); } // 仅转义属性里的引号(文本已在 escapeHtml 中转义过 < > &) function attrSafe(s) { return String(s).replace(/"/g, '"'); } // URL 白名单:仅允许 http/https/mailto/tel/锚点/相对路径;拦截 javascript:/vbscript:/危险 data: function safeUrl(url) { var u = String(url || '').trim(); if (/^\s*(javascript|vbscript):/i.test(u)) return '#'; if (/^\s*data:/i.test(u) && !/^data:image\//i.test(u)) return '#'; return u; } // 行内解析:输入为「未转义」的 markdown 文本 function parseInline(text) { var codes = []; // 1. 先抽出行内代码,避免内部内容被后续规则破坏 text = String(text).replace(/`([^`]+)`/g, function (_m, c) { codes.push(c); return 'CODE' + (codes.length - 1) + ''; }); // 2. 转义 HTML(代码占位符只含字母数字与空字符,不受影响) text = escapeHtml(text); // 3. 图片 ![alt](url "title") text = text.replace(/!\[([^\]]*)\]\(((?:[^()\s]|\([^()]*\))+)(?:\s+"([^)]*?)")?\)/g, function (_m, alt, url, title) { var t = title ? ' title="' + attrSafe(title) + '"' : ''; return '' + attrSafe(alt) + ''; }); // 4. 链接 [text](url "title") text = text.replace(/\[([^\]]+)\]\(((?:[^()\s]|\([^()]*\))+)(?:\s+"([^)]*?)")?\)/g, function (_m, txt, url, title) { var t = title ? ' title="' + attrSafe(title) + '"' : ''; var ext = /^(https?:)?\/\//i.test(url); var extAttr = ext ? ' target="_blank" rel="noopener noreferrer"' : ''; return '' + txt + ''; }); // 5. 粗体 **x** / __x__ text = text.replace(/\*\*([^*]+)\*\*/g, '$1'); text = text.replace(/__([^_]+)__/g, '$1'); // 6. 斜体 *x* / _x_ text = text.replace(/\*([^*]+)\*/g, '$1'); text = text.replace(/(^|[^\w])_([^_]+)_(?=[^\w]|$)/g, '$1$2'); // 7. 删除线 ~~x~~ text = text.replace(/~~([^~]+)~~/g, '$1'); // 8. 还原行内代码 text = text.replace(/CODE(\d+)/g, function (_m, n) { return '' + escapeHtml(codes[+n]) + ''; }); return text; } // 列表解析:返回 { html, next } function parseList(lines, start) { var first = lines[start]; var m = /^(\s*)([-*+]|\d+\.)\s+(.*)$/.exec(first); var baseIndent = m[1].length; var ordered = /\d+\./.test(m[2]); var items = []; var i = start; while (i < lines.length) { var line = lines[i]; if (/^\s*$/.test(line)) break; // 空行结束列表 var mm = /^(\s*)([-*+]|\d+\.)\s+(.*)$/.exec(line); if (!mm) { // 同级的后续非标记行视为当前条目的延续(懒继续) if (items.length && /^\s*\S/.test(line) && !/^\s*[-*+]\s/.test(line) && !/^\s*\d+\.\s/.test(line)) { items[items.length - 1].text.push(line.trim()); i++; continue; } break; } var ind = mm[1].length; if (ind < baseIndent) break; // 减缩进,结束 if (ind > baseIndent) { // 嵌套列表 var sub = parseList(lines, i); items[items.length - 1].nested += sub.html; i = sub.next; continue; } // 同级新条目 items.push({ text: [mm[3]], nested: '' }); i++; } var tag = ordered ? 'ol' : 'ul'; var out = '<' + tag + '>'; for (var k = 0; k < items.length; k++) { out += '
  • ' + parseInline(items[k].text.join(' ')); if (items[k].nested) out += items[k].nested; out += '
  • '; } out += ''; return { html: out, next: i }; } function splitRow(line) { var t = line.trim(); if (t.charAt(0) === '|') t = t.slice(1); if (t.charAt(t.length - 1) === '|') t = t.slice(0, -1); return t.split('|').map(function (c) { return c.trim(); }); } // 表格解析:返回 { html, next } function parseTable(lines, start) { var header = splitRow(lines[start]); var sep = lines[start + 1]; var aligns = splitRow(sep).map(function (c) { var left = c.charAt(0) === ':'; var right = c.charAt(c.length - 1) === ':'; if (left && right) return 'center'; if (right) return 'right'; if (left) return 'left'; return ''; }); var rows = []; var j = start + 2; while (j < lines.length && /\|/.test(lines[j]) && lines[j].trim() !== '') { rows.push(splitRow(lines[j])); j++; } function cell(tag, c, idx) { var a = aligns[idx] ? ' style="text-align:' + aligns[idx] + '"' : ''; return '<' + tag + a + '>' + parseInline(c) + ''; } var html = ''; for (var h = 0; h < header.length; h++) html += cell('th', header[h], h); html += ''; for (var r = 0; r < rows.length; r++) { html += ''; for (var c = 0; c < rows[r].length; c++) html += cell('td', rows[r][c], c); html += ''; } html += '
    '; return { html: html, next: j }; } // 块级解析 function parse(src) { src = (src || '').replace(/\r\n?/g, '\n'); var lines = src.split('\n'); var html = []; var para = []; var i = 0; function flushPara() { if (para.length) { var text = para.join('\n'); // 段内软换行 ->
    var inline = parseInline(text).replace(/\n/g, '
    \n'); html.push('

    ' + inline + '

    '); para = []; } } while (i < lines.length) { var line = lines[i]; // 代码围栏 ``` if (/^\s*```/.test(line)) { flushPara(); var lang = line.replace(/^\s*```/, '').trim(); var buf = []; i++; while (i < lines.length && !/^\s*```/.test(lines[i])) { buf.push(lines[i]); i++; } i++; // 跳过结束围栏 html.push('
    ' + escapeHtml(buf.join('\n')) + '
    '); continue; } // 分隔线 if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) { flushPara(); html.push('
    '); i++; continue; } // 标题 # ~ ###### var hm = /^\s*(#{1,6})\s+(.*?)\s*#*\s*$/.exec(line); if (hm) { flushPara(); var lvl = hm[1].length; html.push('' + parseInline(hm[2].trim()) + ''); i++; continue; } // 引用 > if (/^\s*>\s?/.test(line)) { flushPara(); var qbuf = []; while (i < lines.length && /^\s*>\s?/.test(lines[i])) { qbuf.push(lines[i].replace(/^\s*>\s?/, '')); i++; } html.push('
    ' + parse(qbuf.join('\n')) + '
    '); continue; } // 表格(当前行含 |,且下一行是分隔行) if (/\|/.test(line) && i + 1 < lines.length && /^\s*\|?[\s:|-]+\|?\s*$/.test(lines[i + 1]) && /-/.test(lines[i + 1])) { flushPara(); var tbl = parseTable(lines, i); html.push(tbl.html); i = tbl.next; continue; } // 列表 if (/^\s*([-*+]|\d+\.)\s+/.test(line)) { flushPara(); var lst = parseList(lines, i); html.push(lst.html); i = lst.next; continue; } // 空行 if (/^\s*$/.test(line)) { flushPara(); i++; continue; } // 段落文本 para.push(line); i++; } flushPara(); return html.join('\n'); } var api = { parse: parse, escapeHtml: escapeHtml }; if (typeof module !== 'undefined' && module.exports) { module.exports = api; } global.marked = api; })(typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : this));