<template>
|
<div class="markdown-body assistant-markdown break-words" v-html="renderedContent"></div>
|
</template>
|
|
<script setup>
|
const props = defineProps({
|
content: {
|
type: String,
|
default: ''
|
}
|
})
|
|
const renderedContent = computed(() => renderMarkdown(props.content))
|
|
function renderMarkdown(content) {
|
const normalized = normalizeMarkdownSource(content)
|
if (!normalized) {
|
return ''
|
}
|
|
const lines = normalized.split('\n')
|
const html = []
|
|
for (let index = 0; index < lines.length; ) {
|
const line = lines[index].trim()
|
|
if (!line) {
|
index += 1
|
continue
|
}
|
|
if (/^```/.test(line)) {
|
const codeLines = []
|
index += 1
|
while (index < lines.length && !/^```/.test(lines[index].trim())) {
|
codeLines.push(lines[index])
|
index += 1
|
}
|
if (index < lines.length) {
|
index += 1
|
}
|
html.push(`<pre><code>${escapeHtml(codeLines.join('\n'))}</code></pre>`)
|
continue
|
}
|
|
if (/^\s*#{1,6}\s+/.test(line)) {
|
html.push(renderHeadingLine(line))
|
index += 1
|
continue
|
}
|
|
if (/^\s*[-*]\s+/.test(line)) {
|
const listLines = []
|
while (index < lines.length && /^\s*[-*]\s+/.test(lines[index].trim())) {
|
listLines.push(lines[index].trim())
|
index += 1
|
}
|
html.push(`<ul>${listLines
|
.map((item) => `<li>${renderMarkdownInline(item.replace(/^\s*[-*]\s+/, ''))}</li>`)
|
.join('')}</ul>`)
|
continue
|
}
|
|
if (/^\s*\d+\.\s+/.test(line)) {
|
const listLines = []
|
while (index < lines.length && /^\s*\d+\.\s+/.test(lines[index].trim())) {
|
listLines.push(lines[index].trim())
|
index += 1
|
}
|
html.push(`<ol>${listLines
|
.map((item) => `<li>${renderMarkdownInline(item.replace(/^\s*\d+\.\s+/, ''))}</li>`)
|
.join('')}</ol>`)
|
continue
|
}
|
|
if (/^\s*>\s?/.test(line)) {
|
const quoteLines = []
|
while (index < lines.length && /^\s*>\s?/.test(lines[index].trim())) {
|
quoteLines.push(lines[index].trim().replace(/^\s*>\s?/, ''))
|
index += 1
|
}
|
html.push(`<blockquote>${quoteLines.map((item) => `<p>${renderMarkdownInline(item)}</p>`).join('')}</blockquote>`)
|
continue
|
}
|
|
const paragraphLines = []
|
while (index < lines.length) {
|
const current = lines[index].trim()
|
if (
|
!current ||
|
/^```/.test(current) ||
|
/^\s*#{1,6}\s+/.test(current) ||
|
/^\s*[-*]\s+/.test(current) ||
|
/^\s*\d+\.\s+/.test(current) ||
|
/^\s*>\s?/.test(current)
|
) {
|
break
|
}
|
paragraphLines.push(current)
|
index += 1
|
}
|
|
if (paragraphLines.length) {
|
html.push(`<p>${paragraphLines.map((item) => renderMarkdownInline(item)).join('<br>')}</p>`)
|
} else {
|
index += 1
|
}
|
}
|
|
return html.join('')
|
}
|
|
function normalizeMarkdownSource(content) {
|
return String(content || '')
|
.replace(/\r\n/g, '\n')
|
.replace(/<br\s*\/?>/gi, '\n')
|
.replace(/<\/p\s*>/gi, '\n\n')
|
.replace(/<p[^>]*>/gi, '')
|
.replace(/<\/div\s*>/gi, '\n')
|
.replace(/<div[^>]*>/gi, '')
|
.replace(/ /gi, ' ')
|
.replace(/\n{3,}/g, '\n\n')
|
.trim()
|
}
|
|
function renderHeadingLine(line) {
|
const level = Math.min((line.match(/^(\s*#+)/)?.[0].trim().length || 1), 6)
|
const title = line.replace(/^\s*#{1,6}\s+/, '')
|
return `<h${level}>${renderMarkdownInline(title)}</h${level}>`
|
}
|
|
function renderMarkdownInline(value) {
|
let result = escapeHtml(value)
|
result = result.replace(/`([^`]+)`/g, '<code>$1</code>')
|
result = result.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
result = result.replace(/\*([^*\n]+)\*/g, '<em>$1</em>')
|
result = result.replace(
|
/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,
|
'<a href="$2" target="_blank" rel="noreferrer noopener">$1</a>'
|
)
|
return result
|
}
|
|
function escapeHtml(value) {
|
return String(value)
|
.replace(/&/g, '&')
|
.replace(/</g, '<')
|
.replace(/>/g, '>')
|
.replace(/"/g, '"')
|
.replace(/'/g, ''')
|
}
|
</script>
|
|
<style lang="scss">
|
@use '@styles/core/md.scss';
|
</style>
|
|
<style scoped>
|
:deep(.assistant-markdown) {
|
color: var(--art-gray-800);
|
}
|
|
:deep(.assistant-markdown > :first-child) {
|
margin-top: 0;
|
}
|
|
:deep(.assistant-markdown > :last-child) {
|
margin-bottom: 0;
|
}
|
|
:deep(.assistant-markdown p) {
|
margin-bottom: 0.6rem;
|
}
|
|
:deep(.assistant-markdown ul),
|
:deep(.assistant-markdown ol) {
|
padding-left: 1.25rem;
|
}
|
|
:deep(.assistant-markdown pre) {
|
overflow-x: auto;
|
border-radius: 14px;
|
background: var(--el-fill-color-light);
|
padding: 0.875rem 1rem;
|
}
|
|
:deep(.assistant-markdown code) {
|
border-radius: 8px;
|
background: var(--el-fill-color-light);
|
padding: 0.1rem 0.35rem;
|
font-size: 0.875em;
|
}
|
|
:deep(.assistant-markdown pre code) {
|
background: transparent;
|
padding: 0;
|
}
|
</style>
|