|
|
@@ -42,9 +42,9 @@ const renderLatexWithKatex = (latex, displayMode = false) => {
|
|
|
*/
|
|
|
const preprocessLatex = (text) => {
|
|
|
if (!text) return text
|
|
|
-
|
|
|
+
|
|
|
let processed = text
|
|
|
-
|
|
|
+
|
|
|
// 首先处理块级公式 $$...$$(必须在行内公式之前处理)
|
|
|
// 支持多行块级公式
|
|
|
processed = processed.replace(/\$\$([\s\S]+?)\$\$/g, (match, latex) => {
|
|
|
@@ -53,7 +53,7 @@ const preprocessLatex = (text) => {
|
|
|
// 使用特殊标记包裹,防止被 Markdown 解析
|
|
|
return `<div class="katex-block-wrapper">${rendered}</div>`
|
|
|
})
|
|
|
-
|
|
|
+
|
|
|
// 然后处理行内公式 $...$
|
|
|
// 注意:不匹配已经处理过的 katex-block-wrapper 标记
|
|
|
processed = processed.replace(/(?<!<div class="katex-block-wrapper">.*)\$([^\$\n]+?)\$(?!.*<\/div>)/g, (match, latex) => {
|
|
|
@@ -61,21 +61,21 @@ const preprocessLatex = (text) => {
|
|
|
const rendered = renderLatexWithKatex(trimmedLatex, false)
|
|
|
return `<span class="katex-inline-wrapper">${rendered}</span>`
|
|
|
})
|
|
|
-
|
|
|
+
|
|
|
// 处理 \(...\) 格式的行内公式
|
|
|
processed = processed.replace(/\\\((.+?)\\\)/g, (match, latex) => {
|
|
|
const trimmedLatex = latex.trim()
|
|
|
const rendered = renderLatexWithKatex(trimmedLatex, false)
|
|
|
return `<span class="katex-inline-wrapper">${rendered}</span>`
|
|
|
})
|
|
|
-
|
|
|
+
|
|
|
// 处理 \[...\] 格式的块级公式
|
|
|
processed = processed.replace(/\\\[([\s\S]+?)\\\]/g, (match, latex) => {
|
|
|
const trimmedLatex = latex.trim()
|
|
|
const rendered = renderLatexWithKatex(trimmedLatex, true)
|
|
|
return `<div class="katex-block-wrapper">${rendered}</div>`
|
|
|
})
|
|
|
-
|
|
|
+
|
|
|
return processed
|
|
|
}
|
|
|
|
|
|
@@ -85,7 +85,7 @@ const renderer = {
|
|
|
// marked v4+ 传入的是token对象
|
|
|
// 需要从token中提取href和text
|
|
|
let href, text, title
|
|
|
-
|
|
|
+
|
|
|
if (typeof token === 'object' && token !== null) {
|
|
|
// token对象格式
|
|
|
href = token.href || token.url || '#'
|
|
|
@@ -97,12 +97,12 @@ const renderer = {
|
|
|
text = arguments[2] || arguments[1] || href
|
|
|
title = arguments[1] || href
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 确保是字符串类型
|
|
|
href = String(href)
|
|
|
text = String(text)
|
|
|
title = String(title)
|
|
|
-
|
|
|
+
|
|
|
// HTML转义,防止XSS
|
|
|
const escapeHtml = (str) => {
|
|
|
return str
|
|
|
@@ -112,47 +112,47 @@ const renderer = {
|
|
|
.replace(/"/g, '"')
|
|
|
.replace(/'/g, ''')
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
const escapedHref = escapeHtml(href)
|
|
|
const escapedText = escapeHtml(text)
|
|
|
const escapedTitle = escapeHtml(title)
|
|
|
-
|
|
|
+
|
|
|
// 渲染为灰色胶囊样式
|
|
|
const capsuleStyle = `
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
- gap: 4px;
|
|
|
+ gap: 3px;
|
|
|
background-color: #E8EAED;
|
|
|
color: #5F6368;
|
|
|
- font-size: 13px;
|
|
|
- padding: 4px 10px;
|
|
|
- border-radius: 12px;
|
|
|
+ font-size: 11px;
|
|
|
+ padding: 1px 8px;
|
|
|
+ border-radius: 10px;
|
|
|
cursor: pointer;
|
|
|
- margin: 0 4px;
|
|
|
+ margin: 0 2px;
|
|
|
border: 1px solid #DADCE0;
|
|
|
font-weight: 500;
|
|
|
transition: all 0.2s ease;
|
|
|
- line-height: 1.4;
|
|
|
- max-width: 300px;
|
|
|
+ line-height: 1.2;
|
|
|
+ max-width: 260px;
|
|
|
white-space: nowrap;
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
vertical-align: middle;
|
|
|
text-decoration: none;
|
|
|
`.replace(/\s+/g, ' ').trim()
|
|
|
-
|
|
|
+
|
|
|
const hoverStyle = `
|
|
|
this.style.backgroundColor='#D2D4D8';
|
|
|
this.style.borderColor='#BABDBF';
|
|
|
this.style.boxShadow='0 1px 3px rgba(0, 0, 0, 0.1)';
|
|
|
`
|
|
|
-
|
|
|
+
|
|
|
const resetStyle = `
|
|
|
this.style.backgroundColor='#E8EAED';
|
|
|
this.style.borderColor='#DADCE0';
|
|
|
this.style.boxShadow='none';
|
|
|
`
|
|
|
-
|
|
|
+
|
|
|
return `<a
|
|
|
href="${escapedHref}"
|
|
|
class="link-capsule"
|
|
|
@@ -161,7 +161,7 @@ const renderer = {
|
|
|
onmouseover="${hoverStyle}"
|
|
|
onmouseout="${resetStyle}"
|
|
|
title="${escapedTitle}"
|
|
|
- ><span style="font-size: 12px;">🔗</span><span>${escapedText}</span></a>`
|
|
|
+ ><span style="font-size: 9px;">🔗</span><span>${escapedText}</span></a>`
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -181,17 +181,17 @@ marked.use({
|
|
|
*/
|
|
|
export const renderMarkdown = (markdown) => {
|
|
|
if (!markdown) return ''
|
|
|
-
|
|
|
+
|
|
|
try {
|
|
|
// 确保输入是字符串
|
|
|
let markdownText = String(markdown)
|
|
|
-
|
|
|
+
|
|
|
// 预处理 LaTeX 公式(在 Markdown 解析之前)
|
|
|
markdownText = preprocessLatex(markdownText)
|
|
|
-
|
|
|
+
|
|
|
// 渲染 Markdown 为 HTML
|
|
|
const rawHtml = marked.parse(markdownText)
|
|
|
-
|
|
|
+
|
|
|
// 使用 DOMPurify 清理 HTML,防止 XSS 攻击
|
|
|
const cleanHtml = DOMPurify.sanitize(rawHtml, {
|
|
|
ALLOWED_TAGS: [
|
|
|
@@ -204,7 +204,7 @@ export const renderMarkdown = (markdown) => {
|
|
|
'table', 'thead', 'tbody', 'tr', 'th', 'td',
|
|
|
'div', 'span',
|
|
|
// KaTeX 需要的标签 - 完整列表
|
|
|
- 'math', 'semantics', 'mrow', 'mi', 'mn', 'mo', 'mfrac', 'msup', 'msub', 'msubsup',
|
|
|
+ 'math', 'semantics', 'mrow', 'mi', 'mn', 'mo', 'mfrac', 'msup', 'msub', 'msubsup',
|
|
|
'mtext', 'mspace', 'annotation', 'menclose', 'mover', 'munder', 'munderover',
|
|
|
'mtable', 'mtr', 'mtd', 'msqrt', 'mroot', 'mpadded', 'mphantom', 'mglyph',
|
|
|
'svg', 'path', 'line', 'rect', 'circle', 'use', 'g', 'defs', 'symbol', 'foreignObject'
|
|
|
@@ -220,7 +220,7 @@ export const renderMarkdown = (markdown) => {
|
|
|
// 允许所有 data-* 属性
|
|
|
ALLOW_DATA_ATTR: true
|
|
|
})
|
|
|
-
|
|
|
+
|
|
|
return cleanHtml
|
|
|
} catch (error) {
|
|
|
console.error('Markdown 渲染失败:', error)
|