refactor: 第三方库替换 & 代码精简 — v5.1.5
- Markdown 解析器:内联正则 → marked 库(完整 GFM 支持) - HTML 净化器:内联 DOM 遍历 → DOMPurify(业界标准) - crypto.ts:删除 sha256js/xorBytes 死代码回退路径 - 消除 5 个文件中 escapeHtml/formatSize 重复定义 - 版本号升级至 5.1.5
This commit is contained in:
@@ -1,84 +1,46 @@
|
||||
/**
|
||||
* Markdown 渲染器配置
|
||||
* Markdown 渲染器配置 — 基于 marked 库
|
||||
*/
|
||||
|
||||
import { SAFE_URI_SCHEMES } from './sanitizer.js';
|
||||
import { marked as markedLib, Renderer } from 'marked';
|
||||
import { sanitize } from './sanitizer.js';
|
||||
|
||||
// 简化的 Markdown 解析器(内联,不依赖外部 marked 库)
|
||||
// 支持基本 GFM 语法:标题、粗体、斜体、代码块、链接、列表、表格
|
||||
// 自定义渲染器:链接安全检查
|
||||
const renderer = new Renderer();
|
||||
const SAFE_SCHEMES = new Set(['http:', 'https:', 'mailto:', 'tel:', 'ftp:', 'ftps:', 'xmpp:']);
|
||||
|
||||
function parseMarkdown(md: string): string {
|
||||
renderer.link = function ({ href, text, tokens }) {
|
||||
try {
|
||||
const protocol = new URL(href, 'https://placeholder').protocol;
|
||||
if (!SAFE_SCHEMES.has(protocol)) {
|
||||
return `<span class="blocked-link">${text}</span>`;
|
||||
}
|
||||
} catch { /* 允许相对路径 */ }
|
||||
return `<a href="${href}" target="_blank" rel="noopener noreferrer">${text}</a>`;
|
||||
};
|
||||
|
||||
renderer.image = function ({ href, text }) {
|
||||
try {
|
||||
const protocol = new URL(href, 'https://placeholder').protocol;
|
||||
if (!SAFE_SCHEMES.has(protocol)) {
|
||||
return `<span class="blocked-link">${text || '图片'}</span>`;
|
||||
}
|
||||
} catch { /* 允许相对路径 */ }
|
||||
return `<img src="${href}" alt="${text || ''}" loading="lazy">`;
|
||||
};
|
||||
|
||||
// 配置 marked
|
||||
markedLib.use({
|
||||
renderer,
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* 解析 Markdown 并净化 HTML 输出
|
||||
*/
|
||||
export function marked(md: string): string {
|
||||
if (!md) return '';
|
||||
let html = escapeForMd(md);
|
||||
|
||||
// 代码块 ```...```
|
||||
const codeBlockRe = new RegExp('```(\\w*)\\n([\\s\\S]*?)```', 'g');
|
||||
html = html.replace(codeBlockRe, (_m, lang, code) => {
|
||||
return `<pre><code class="language-${lang}">${code.trim()}</code></pre>`;
|
||||
});
|
||||
|
||||
// 行内代码
|
||||
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
|
||||
// 标题
|
||||
html = html.replace(/^######\s+(.+)$/gm, '<h6>$1</h6>');
|
||||
html = html.replace(/^#####\s+(.+)$/gm, '<h5>$1</h5>');
|
||||
html = html.replace(/^####\s+(.+)$/gm, '<h4>$1</h4>');
|
||||
html = html.replace(/^###\s+(.+)$/gm, '<h3>$1</h3>');
|
||||
html = html.replace(/^##\s+(.+)$/gm, '<h2>$1</h2>');
|
||||
html = html.replace(/^#\s+(.+)$/gm, '<h1>$1</h1>');
|
||||
|
||||
// 粗体+斜体
|
||||
html = html.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
|
||||
// 粗体
|
||||
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
||||
// 斜体
|
||||
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
||||
// 删除线
|
||||
html = html.replace(/~~(.+?)~~/g, '<del>$1</del>');
|
||||
|
||||
// 链接 [text](url)
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, text, href) => {
|
||||
try {
|
||||
const safe = SAFE_URI_SCHEMES.has(new URL(href, 'https://placeholder').protocol);
|
||||
if (!safe) return `<span class="blocked-link">${text}</span>`;
|
||||
} catch { /* allow relative */ }
|
||||
return `<a href="${href}" target="_blank" rel="noopener noreferrer">${text}</a>`;
|
||||
});
|
||||
|
||||
// 图片 
|
||||
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" loading="lazy">');
|
||||
|
||||
// 引用块
|
||||
html = html.replace(/^>\s+(.+)$/gm, '<blockquote><p>$1</p></blockquote>');
|
||||
|
||||
// 水平线
|
||||
html = html.replace(/^---+$/gm, '<hr>');
|
||||
|
||||
// 无序列表
|
||||
html = html.replace(/^[\-\*]\s+(.+)$/gm, '<li>$1</li>');
|
||||
html = html.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
|
||||
|
||||
// 有序列表
|
||||
html = html.replace(/^\d+\.\s+(.+)$/gm, '<li>$1</li>');
|
||||
|
||||
// 换行
|
||||
html = html.replace(/\n\n/g, '</p><p>');
|
||||
html = html.replace(/\n/g, '<br>');
|
||||
|
||||
// 包裹在 p 标签中
|
||||
if (!html.startsWith('<')) {
|
||||
html = '<p>' + html + '</p>';
|
||||
}
|
||||
|
||||
return html;
|
||||
const raw = markedLib.parse(md) as string;
|
||||
return sanitize(raw);
|
||||
}
|
||||
|
||||
function escapeForMd(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
export { parseMarkdown as marked };
|
||||
|
||||
Reference in New Issue
Block a user