Commit 8c7b2c33 authored by tyyin lan's avatar tyyin lan

feat: markdown render代码模块支持内容复制

parent 92e219a6
<script setup lang="ts">
import { computed, ref, watchEffect } from 'vue'
import { computed, nextTick, ref, useTemplateRef, watchEffect } from 'vue'
import { Marked } from 'marked'
import { markedHighlight } from 'marked-highlight'
import 'katex/dist/katex.min.css'
import { throttle } from 'lodash-es'
import { throttle, debounce } from 'lodash-es'
import DOMPurify from 'dompurify'
import hljs from 'highlight.js'
import 'highlight.js/styles/panda-syntax-light.css'
import 'github-markdown-css'
import markedKatex, { MarkedKatexOptions } from 'marked-katex-extension'
import { copyToClip } from '@/utils/copy'
import { useI18n } from 'vue-i18n'
interface Props {
rawTextContent: string
......@@ -29,19 +31,47 @@ const katexOptions: MarkedKatexOptions = {
output: 'html',
}
const marked = new Marked().use(
markedHighlight({
emptyLangClass: 'hljs',
langPrefix: 'hljs language-',
highlight(code, lang, _info) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext'
return hljs.highlight(code, { language }).value
const markdownRenderContainerRef = useTemplateRef<HTMLDivElement>('markdownRenderContainerRef')
const { t } = useI18n()
const marked = new Marked()
.use({ gfm: true, async: true, breaks: true })
.use(
markedHighlight({
emptyLangClass: 'hljs',
langPrefix: 'hljs language-',
highlight(code, lang, _info) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext'
// return hljs.highlight(code, { language }).value
const str = hljs.highlight(code, { language }).value
return `<div class="code-render-container"><div class="code-operation-bar-container"><span>${language}</span><span class="code-copy-btn iconfont icon-copy"></span></div><pre class="code-render-inner"><code>${str}</code></pre></div>`
},
}),
)
.use(markedKatex(katexOptions))
.use({
hooks: {
preprocess(markdown: string) {
/**
* 1. 将 \(...\) 和 \\(...\\) 转为 $...$ 以支持行内公式
* 2. 将 \[...\] 和 \\[...\\] 转为 $$...$$ 以支持块级公式
*/
const katexTextReplace = markdown
.replace(/\\\\\(|\\\\\)|\\\(|\\\)/g, '$')
.replace(/\\\\\[|\\\\\]|\\\[|\\\]/g, '$$$$')
return katexTextReplace
},
postprocess(html: string) {
return DOMPurify.sanitize(html)
},
},
}),
{ gfm: true, async: true, breaks: true },
markedKatex(katexOptions),
)
})
let btnEventController = new AbortController()
const renderTextContent = ref('')
......@@ -52,28 +82,54 @@ const articleContainerStyle = computed(() => {
}
})
const katexDelimiters = (text: string) => {
/**
* 1. 将 \(...\) 和 \\(...\\) 转为 $...$ 以支持行内公式
* 2. 将 \[...\] 和 \\[...\\] 转为 $$...$$ 以支持块级公式
*/
const replaceKatexText = text.replace(/\\\\\(|\\\\\)|\\\(|\\\)/g, '$').replace(/\\\\\[|\\\\\]|\\\[|\\\]/g, '$$$$')
return replaceKatexText
}
const textContentParser = throttle(
(text: string) => {
;(marked.parse(text) as Promise<string>).then((res) => {
renderTextContent.value = DOMPurify.sanitize(res)
renderTextContent.value = res
})
},
500,
{ leading: true, trailing: true },
)
const codeCopyBtnEventBind = debounce(
() => {
if (markdownRenderContainerRef.value) {
const codeCopyBtnElList = markdownRenderContainerRef.value.getElementsByClassName('code-copy-btn')
btnEventController.abort()
btnEventController = new AbortController()
/* 遍历 */
;[...codeCopyBtnElList].forEach((elItem) => {
elItem.addEventListener(
'click',
() => {
const code = elItem.parentElement?.nextElementSibling?.firstChild?.textContent
if (code) {
copyToClip(code).then(() => {
window.$message.success(t('common_module.copy_success_message'))
})
}
},
{ signal: btnEventController.signal },
)
})
}
},
1000,
{ leading: false, trailing: true },
)
watchEffect(() => {
const text = katexDelimiters(props.rawTextContent)
textContentParser(text)
textContentParser(props.rawTextContent)
nextTick(() => {
setTimeout(() => {
codeCopyBtnEventBind()
}, 60)
})
})
function getRenderTextContent() {
......@@ -86,19 +142,53 @@ defineExpose({
</script>
<template>
<div class="markdown-render-container">
<article class="markdown-body article-container" :style="articleContainerStyle" v-html="renderTextContent" />
<div ref="markdownRenderContainerRef" class="markdown-render-container">
<article class="markdown-body markdown-render-inner" :style="articleContainerStyle" v-html="renderTextContent" />
</div>
</template>
<style lang="scss" scoped>
@include custom-scrollbar(6px);
.article-container {
overflow-x: auto;
font-family: 'Microsoft YaHei UI';
word-break: break-all;
background-color: unset;
.markdown-render-container {
.markdown-render-inner {
overflow-x: auto;
font-family: 'Microsoft YaHei UI';
word-break: break-all;
background-color: unset;
& > :deep(pre) {
padding: 0;
}
}
:deep(.code-render-container) {
font-family: 'Microsoft YaHei UI';
font-size: v-bind('props.fontSize');
.code-operation-bar-container {
display: flex;
justify-content: space-between;
padding: 10px 16px;
user-select: none;
background-color: #f1f1f1;
.code-copy-btn {
cursor: pointer;
transition: color ease-in-out 0.3s;
&:hover {
color: #777ef9;
}
}
}
.code-render-inner {
padding: 10px 16px 0;
margin: 0;
overflow-x: auto;
}
}
}
:deep(pre) {
......
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