/*
* 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. 图片 
text = text.replace(/!\[([^\]]*)\]\(((?:[^()\s]|\([^()]*\))+)(?:\s+"([^)]*?)")?\)/g, function (_m, alt, url, title) {
var t = title ? ' title="' + attrSafe(title) + '"' : '';
return '';
});
// 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 += '
' + 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('' + 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));