文件
2026-08-16 17:03:10 +08:00

235 行
9.2 KiB
JavaScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
(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\(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?:)?\/\(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));