Files
freeshop/assets/marked.js
T
2026-08-14 10:55:06 +08:00

264 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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, '&lt;')
.replace(/>/g, '&gt;');
}
// 仅转义属性里的引号(文本已在 escapeHtml 中转义过 < > &
function attrSafe(s) {
return String(s).replace(/"/g, '&quot;');
}
// 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+&quot;([^)]*?)&quot;)?\)/g, function (_m, alt, url, title) {
var t = title ? ' title="' + attrSafe(title) + '"' : '';
return '<img src="' + attrSafe(safeUrl(url)) + '" alt="' + attrSafe(alt) + '"' + t + '>';
});
// 4. 链接 [text](url "title")
text = text.replace(/\[([^\]]+)\]\(((?:[^()\s]|\([^()]*\))+)(?:\s+&quot;([^)]*?)&quot;)?\)/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 '<a href="' + attrSafe(safeUrl(url)) + '"' + t + extAttr + '>' + txt + '</a>';
});
// 5. 粗体 **x** / __x__
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
text = text.replace(/__([^_]+)__/g, '<strong>$1</strong>');
// 6. 斜体 *x* / _x_
text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
text = text.replace(/(^|[^\w])_([^_]+)_(?=[^\w]|$)/g, '$1<em>$2</em>');
// 7. 删除线 ~~x~~
text = text.replace(/~~([^~]+)~~/g, '<del>$1</del>');
// 8. 还原行内代码
text = text.replace(/CODE(\d+)/g, function (_m, n) {
return '<code>' + escapeHtml(codes[+n]) + '</code>';
});
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 += '<li>' + parseInline(items[k].text.join(' '));
if (items[k].nested) out += items[k].nested;
out += '</li>';
}
out += '</' + tag + '>';
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) + '</' + tag + '>';
}
var html = '<table><thead><tr>';
for (var h = 0; h < header.length; h++) html += cell('th', header[h], h);
html += '</tr></thead><tbody>';
for (var r = 0; r < rows.length; r++) {
html += '<tr>';
for (var c = 0; c < rows[r].length; c++) html += cell('td', rows[r][c], c);
html += '</tr>';
}
html += '</tbody></table>';
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');
// 段内软换行 -> <br>
var inline = parseInline(text).replace(/\n/g, '<br>\n');
html.push('<p>' + inline + '</p>');
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('<pre><code' +
(lang ? ' class="language-' + attrSafe(lang) + '"' : '') +
'>' + escapeHtml(buf.join('\n')) + '</code></pre>');
continue;
}
// 分隔线
if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
flushPara();
html.push('<hr>');
i++;
continue;
}
// 标题 # ~ ######
var hm = /^\s*(#{1,6})\s+(.*?)\s*#*\s*$/.exec(line);
if (hm) {
flushPara();
var lvl = hm[1].length;
html.push('<h' + lvl + '>' + parseInline(hm[2].trim()) + '</h' + lvl + '>');
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('<blockquote>' + parse(qbuf.join('\n')) + '</blockquote>');
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));