Commit 9ee6142b authored by nick zheng's avatar nick zheng

Merge branch 'beta' into 'master'

feat: markdown渲染支持图表

See merge request !234
parents 8c03792c 3761062f
...@@ -37,8 +37,10 @@ ...@@ -37,8 +37,10 @@
"marked": "^15.0.0", "marked": "^15.0.0",
"marked-highlight": "^2.2.1", "marked-highlight": "^2.2.1",
"marked-katex-extension": "^5.1.4", "marked-katex-extension": "^5.1.4",
"mermaid": "^11.6.0",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"nanoid": "^5.0.7", "nanoid": "^5.0.7",
"panzoom": "^9.4.3",
"pinia": "^2.2.2", "pinia": "^2.2.2",
"qs": "^6.14.0", "qs": "^6.14.0",
"quill": "^2.0.3", "quill": "^2.0.3",
......
This diff is collapsed.
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1748501615425" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4103" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M773.029647 202.029176a79.058824 79.058824 0 0 1 79.058824 79.058824v491.941647a79.058824 79.058824 0 0 1-79.058824 79.058824H281.088a79.058824 79.058824 0 0 1-79.058824-79.058824V281.088a79.058824 79.058824 0 0 1 79.058824-79.058824h491.941647z m0 52.705883H281.088a26.352941 26.352941 0 0 0-26.352941 26.352941v491.941647c0 14.546824 11.806118 26.352941 26.352941 26.352941h491.941647a26.352941 26.352941 0 0 0 26.352941-26.352941V281.088a26.352941 26.352941 0 0 0-26.352941-26.352941z" fill="#999999" p-id="4104"></path><path d="M644.999529 459.685647a61.500235 61.500235 0 0 1 80.353883-5.722353l116.193882 87.160471a26.352941 26.352941 0 0 1-31.623529 42.164706l-116.163765-87.130353a8.794353 8.794353 0 0 0-11.504941 0.813176l-83.84753 83.84753a26.352941 26.352941 0 1 1-37.285647-37.25553l83.877647-83.877647zM403.907765 402.160941a26.352941 26.352941 0 0 1 35.478588 38.972235l-193.264941 175.706353a26.352941 26.352941 0 0 1-35.448471-39.002353l193.234824-175.676235z" fill="#999999" p-id="4105"></path><path d="M192.180706 192.180706a26.352941 26.352941 0 0 1 37.285647 0l632.470588 632.470588a26.352941 26.352941 0 1 1-37.285647 37.285647l-632.470588-632.470588a26.352941 26.352941 0 0 1 0-37.285647z" fill="#999999" p-id="4106"></path></svg>
\ No newline at end of file
...@@ -241,4 +241,58 @@ defineExpose({ ...@@ -241,4 +241,58 @@ defineExpose({
border-radius: 2px; border-radius: 2px;
} }
} }
:deep(.mermaid) {
background-color: #fff;
.mermaid-viewer {
position: relative;
display: flex;
align-items: center;
justify-content: center;
max-width: 100%;
height: 300px;
overflow: hidden;
background-image: radial-gradient(circle, #e4e4e48c 2px, #0000 0);
background-size: 30px 30px;
border-radius: 12px;
svg {
display: block;
max-width: 100% !important;
}
}
}
:deep(.mermaid-error) {
display: flex;
gap: 5px;
align-items: center;
justify-content: center;
padding: 20px 10px;
font-family: 'Microsoft YaHei UI';
font-size: 14px;
color: #999;
}
:deep(.mermaid-rendering-container) {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-width: 200px;
height: 300px;
background-color: #fff;
.mermaid-rendering-content {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #999;
background-image: radial-gradient(circle, #e4e4e48c 2px, #0000 0);
background-size: 30px 30px;
}
}
</style> </style>
...@@ -2,7 +2,13 @@ import { Marked } from 'marked' ...@@ -2,7 +2,13 @@ import { Marked } from 'marked'
import { markedHighlight } from 'marked-highlight' import { markedHighlight } from 'marked-highlight'
import DOMPurify from 'dompurify' import DOMPurify from 'dompurify'
import hljs from 'highlight.js' import hljs from 'highlight.js'
import mermaid from 'mermaid'
import panzoom from 'panzoom'
import { nanoid } from 'nanoid'
import markedKatex, { type MarkedKatexOptions } from 'marked-katex-extension' import markedKatex, { type MarkedKatexOptions } from 'marked-katex-extension'
import i18n from '@/locales'
const t = i18n.global.t
const katexOptions: MarkedKatexOptions = { const katexOptions: MarkedKatexOptions = {
throwOnError: false, throwOnError: false,
...@@ -11,14 +17,121 @@ const katexOptions: MarkedKatexOptions = { ...@@ -11,14 +17,121 @@ const katexOptions: MarkedKatexOptions = {
output: 'html', output: 'html',
} }
// Mermaid初始化
mermaid.initialize({
startOnLoad: false,
htmlLabels: true,
suppressErrorRendering: true,
theme: 'default',
securityLevel: 'loose',
fontFamily: 'Microsoft YaHei UI',
altFontFamily: 'Microsoft YaHei UI',
fontSize: 14,
themeVariables: {
textColor: '#333',
pie1: '#94d4c0',
pie2: '#fdaf91',
pie3: '#afbddb',
pie4: '#eeadd5',
pieSectionTextSize: '16px',
pieLegendTextSize: '16px',
pieTitleTextSize: '24px',
},
pie: {
textPosition: 0.6,
},
flowchart: {
useMaxWidth: false,
},
})
// 创建Mermaid扩展
const createMermaidExtension = () => ({
name: 'mermaid',
level: 'block' as const,
start(src: string) {
return src.match(/^```mermaid/)?.index
},
tokenizer(src: string) {
const match = src.match(/^```mermaid\n([\s\S]*?)\n```/)
if (match) {
return {
type: 'mermaid',
raw: match[0],
text: match[1].trim(),
}
}
return undefined
},
renderer(token: { text?: string }) {
return `<pre class="mermaid">${token.text}</pre>`
},
})
const walkTokens = async (token: any) => {
switch (token.type) {
case 'mermaid': {
try {
const mermaidId = `mermaid-${nanoid()}`
const { svg } = await mermaid.render(mermaidId, token.text)
const tempDiv = document.createElement('div')
tempDiv.innerHTML = `<div class="mermaid-viewer">${svg}</div>`
const svgEl = tempDiv.querySelector('.mermaid-viewer svg')
if (svgEl) {
svgEl.setAttribute('height', '100%')
svgEl.setAttribute('width', '100%')
}
token.text = tempDiv.innerHTML
} catch (error) {
token.text = `<div class="mermaid-error">
<div class="bg-svg-mermaid_error h-5 w-5"></div>
<span>${t('common_module.mermaid_render_error')}</span>
</div>`
}
break
}
}
}
export function initMermaidPanzoom() {
setTimeout(() => {
const mermaidViewers = document.querySelectorAll('.mermaid-viewer')
mermaidViewers.forEach((viewer) => {
const svgEl = viewer.querySelector('svg')
if (svgEl) {
panzoom(svgEl as SVGElement, {
maxZoom: 5,
minZoom: 0.2,
bounds: true,
boundsPadding: 0.1,
})
}
})
}, 0)
}
export function createMarkedInst() { export function createMarkedInst() {
return new Marked() return new Marked()
.use({ gfm: true, async: true, breaks: true }) .use({
gfm: true,
async: true,
breaks: true,
extensions: [createMermaidExtension()],
})
.use( .use(
markedHighlight({ markedHighlight({
emptyLangClass: 'hljs', emptyLangClass: 'hljs',
langPrefix: 'hljs code-container-wrapper language-', langPrefix: 'hljs code-container-wrapper language-',
highlight(code, lang, _info) { highlight(code, lang, _info) {
if (lang === 'mermaid') {
return `<div class="mermaid-rendering-container">
<div class="mermaid-rendering-content">${t('common_module.mermaid_rendering')}</div>
</div>`
}
const language = hljs.getLanguage(lang) ? lang : 'plaintext' const language = hljs.getLanguage(lang) ? lang : 'plaintext'
// return hljs.highlight(code, { language }).value // return hljs.highlight(code, { language }).value
...@@ -38,6 +151,7 @@ export function createMarkedInst() { ...@@ -38,6 +151,7 @@ export function createMarkedInst() {
}), }),
) )
.use(markedKatex(katexOptions)) .use(markedKatex(katexOptions))
.use({ walkTokens, async: true })
.use({ .use({
hooks: { hooks: {
preprocess(markdown: string) { preprocess(markdown: string) {
...@@ -53,12 +167,41 @@ export function createMarkedInst() { ...@@ -53,12 +167,41 @@ export function createMarkedInst() {
return katexTextReplace return katexTextReplace
}, },
postprocess(html: string) { postprocess(html: string) {
return DOMPurify.sanitize(html) initMermaidPanzoom()
return getSanitizedHtml(html)
}, },
}, },
}) })
} }
// 获取净化HTML, 排除Mermaid render的svg标签
function getSanitizedHtml(html: string) {
const mermaidElementRegex = /<div class="mermaid-viewer"><svg(.*?)>(.*?)<\/svg><\/div>/gs
const mermaidObjects: { placeholder: string; content: string }[] = []
let processedHtml = html
let match: RegExpExecArray | null = null
let i = 0
while ((match = mermaidElementRegex.exec(html)) !== null) {
const fullMatch = match[0]
const placeholder = `MERMAID_PLACEHOLDER_${i}`
mermaidObjects.push({ placeholder, content: fullMatch })
processedHtml = processedHtml.replace(fullMatch, placeholder)
i++
}
const sanitizedHtmlWithoutMermaid = DOMPurify.sanitize(processedHtml)
let sanitizedHtml = sanitizedHtmlWithoutMermaid
for (const mermaidItem of mermaidObjects) {
sanitizedHtml = sanitizedHtml.replace(mermaidItem.placeholder, mermaidItem.content)
}
return sanitizedHtml
}
export function markdownParser(markdown: string) { export function markdownParser(markdown: string) {
return createMarkedInst().parse(markdown) return createMarkedInst().parse(markdown)
} }
...@@ -165,6 +165,8 @@ common_module: ...@@ -165,6 +165,8 @@ common_module:
output_parameter: 'Output parameter' output_parameter: 'Output parameter'
submit_successfully: 'Submit successfully' submit_successfully: 'Submit successfully'
submit_failure: 'Submit failure' submit_failure: 'Submit failure'
mermaid_render_error: 'There is an error in the chart rendering. Please ask again'
mermaid_rendering: 'The chart is being rendered...'
dialogue_module: dialogue_module:
continue_question_message: 'You can keep asking questions' continue_question_message: 'You can keep asking questions'
......
...@@ -164,6 +164,8 @@ common_module: ...@@ -164,6 +164,8 @@ common_module:
output_parameter: '输出参数' output_parameter: '输出参数'
submit_successfully: '提交成功' submit_successfully: '提交成功'
submit_failure: '提交失败' submit_failure: '提交失败'
mermaid_render_error: '图表渲染出错了,请重新提问'
mermaid_rendering: '图表正在渲染中...'
dialogue_module: dialogue_module:
continue_question_message: '你可以继续提问' continue_question_message: '你可以继续提问'
......
...@@ -164,6 +164,8 @@ common_module: ...@@ -164,6 +164,8 @@ common_module:
output_parameter: '輸出參數' output_parameter: '輸出參數'
submit_successfully: '提交成功' submit_successfully: '提交成功'
submit_failure: '提交失敗' submit_failure: '提交失敗'
mermaid_render_error: '圖表渲染出錯了,請重新提問'
mermaid_rendering: '圖表正在渲染中...'
dialogue_module: dialogue_module:
continue_question_message: '你可以繼續提問' continue_question_message: '你可以繼續提問'
......
...@@ -164,6 +164,8 @@ declare namespace I18n { ...@@ -164,6 +164,8 @@ declare namespace I18n {
output_parameter: string output_parameter: string
submit_successfully: string submit_successfully: string
submit_failure: string submit_failure: string
mermaid_render_error: string
mermaid_rendering: string
dialogue_module: { dialogue_module: {
continue_question_message: string continue_question_message: string
......
...@@ -46,6 +46,7 @@ export default defineConfig(({ command, mode }) => { ...@@ -46,6 +46,7 @@ export default defineConfig(({ command, mode }) => {
'marked-highlight': ['marked-highlight'], 'marked-highlight': ['marked-highlight'],
echarts: ['echarts'], echarts: ['echarts'],
katex: ['katex'], katex: ['katex'],
mermaid: ['mermaid'],
}, },
}, },
}, },
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment