<template>
<div v-html="renderContent" class="markdown-renderer" :class="themeClass"></div>
</template>
<script setup lang="ts">
import { computed, type PropType } from 'vue'
import DOMPurify from 'dompurify'
import MarkdownIt from 'markdown-it'
import type { Options } from 'markdown-it'
import hljs from 'highlight.js/lib/core'
import { extractMessageText } from '../../utils'
import 'highlight.js/styles/github.css'
import bash from 'highlight.js/lib/languages/bash'
import javascript from 'highlight.js/lib/languages/javascript'
import typescript from 'highlight.js/lib/languages/typescript'
import json from 'highlight.js/lib/languages/json'
import yaml from 'highlight.js/lib/languages/yaml'
import xml from 'highlight.js/lib/languages/xml'
import shell from 'highlight.js/lib/languages/shell'
hljs.registerLanguage('bash', bash)
hljs.registerLanguage('javascript', javascript)
hljs.registerLanguage('typescript', typescript)
hljs.registerLanguage('json', json)
hljs.registerLanguage('yaml', yaml)
hljs.registerLanguage('xml', xml)
hljs.registerLanguage('shell', shell)
interface MarkdownMessage {
content: unknown
}
const props = defineProps({
message: {
type: Object as PropType<MarkdownMessage>,
default: () => ({ content: '' })
},
theme: {
type: String as PropType<'light' | 'dark'>,
default: 'light'
},
options: {
type: Object as PropType<Options>,
default: () => ({})
}
})
const themeClass = computed(() => `hljs-theme-${props.theme}`)
const escapeHtml = (s: string) =>
s.replace(/[&<>"']/g, (ch) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch]!))
const markdownIt = new MarkdownIt({
html: true,
breaks: true,
typographer: true,
highlight: (str: string, lang: string) => {
if (lang && hljs.getLanguage(lang)) {
try {
const { value } = hljs.highlight(str, { language: lang, ignoreIllegals: true })
return `<pre class="hljs"><code class="language-${lang}">${value}</code></pre>`
} catch (e) {
}
}
return `<pre class="hljs"><code>${escapeHtml(str)}</code></pre>`
},
...props.options
})
const renderContent = computed(() => {
let content = ''
if (typeof props.message.content === 'string') {
content = props.message.content
} else if (Array.isArray(props.message.content)) {
content = extractMessageText(props.message.content)
}
return DOMPurify.sanitize(markdownIt.render(content))
})
</script>
<style lang="less">
.markdown-renderer {
word-break: break-word;
pre {
border-radius: 6px;
padding: 1em;
overflow: auto;
line-height: 1.45;
> code {
font-size: 12px;
}
}
// TODO: 适配TInyEngine主题,实现跟随主题自动切换
&.hljs-theme-light {
pre {
background-color: #f6f8fa;
}
}
&.hljs-theme-dark {
pre {
background-color: #0d1117;
}
}
> *:first-child {
margin-top: 0 !important;
}
> *:last-child {
margin-bottom: 0 !important;
}
}
</style>