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

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

parent 92e219a6
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watchEffect } from 'vue' import { computed, nextTick, ref, useTemplateRef, watchEffect } from 'vue'
import { Marked } from 'marked' import { Marked } from 'marked'
import { markedHighlight } from 'marked-highlight' import { markedHighlight } from 'marked-highlight'
import 'katex/dist/katex.min.css' import 'katex/dist/katex.min.css'
import { throttle } from 'lodash-es' import { throttle, debounce } from 'lodash-es'
import DOMPurify from 'dompurify' import DOMPurify from 'dompurify'
import hljs from 'highlight.js' import hljs from 'highlight.js'
import 'highlight.js/styles/panda-syntax-light.css' import 'highlight.js/styles/panda-syntax-light.css'
import 'github-markdown-css' import 'github-markdown-css'
import markedKatex, { MarkedKatexOptions } from 'marked-katex-extension' import markedKatex, { MarkedKatexOptions } from 'marked-katex-extension'
import { copyToClip } from '@/utils/copy'
import { useI18n } from 'vue-i18n'
interface Props { interface Props {
rawTextContent: string rawTextContent: string
...@@ -29,19 +31,47 @@ const katexOptions: MarkedKatexOptions = { ...@@ -29,19 +31,47 @@ const katexOptions: MarkedKatexOptions = {
output: 'html', output: 'html',
} }
const marked = new Marked().use( const markdownRenderContainerRef = useTemplateRef<HTMLDivElement>('markdownRenderContainerRef')
markedHighlight({ const { t } = useI18n()
emptyLangClass: 'hljs',
langPrefix: 'hljs language-', const marked = new Marked()
highlight(code, lang, _info) { .use({ gfm: true, async: true, breaks: true })
const language = hljs.getLanguage(lang) ? lang : 'plaintext' .use(
markedHighlight({
return hljs.highlight(code, { language }).value 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('') const renderTextContent = ref('')
...@@ -52,28 +82,54 @@ const articleContainerStyle = computed(() => { ...@@ -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( const textContentParser = throttle(
(text: string) => { (text: string) => {
;(marked.parse(text) as Promise<string>).then((res) => { ;(marked.parse(text) as Promise<string>).then((res) => {
renderTextContent.value = DOMPurify.sanitize(res) renderTextContent.value = res
}) })
}, },
500, 500,
{ leading: true, trailing: true }, { 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(() => { watchEffect(() => {
const text = katexDelimiters(props.rawTextContent) textContentParser(props.rawTextContent)
textContentParser(text)
nextTick(() => {
setTimeout(() => {
codeCopyBtnEventBind()
}, 60)
})
}) })
function getRenderTextContent() { function getRenderTextContent() {
...@@ -86,19 +142,53 @@ defineExpose({ ...@@ -86,19 +142,53 @@ defineExpose({
</script> </script>
<template> <template>
<div class="markdown-render-container"> <div ref="markdownRenderContainerRef" class="markdown-render-container">
<article class="markdown-body article-container" :style="articleContainerStyle" v-html="renderTextContent" /> <article class="markdown-body markdown-render-inner" :style="articleContainerStyle" v-html="renderTextContent" />
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@include custom-scrollbar(6px); @include custom-scrollbar(6px);
.article-container { .markdown-render-container {
overflow-x: auto; .markdown-render-inner {
font-family: 'Microsoft YaHei UI'; overflow-x: auto;
word-break: break-all; font-family: 'Microsoft YaHei UI';
background-color: unset; 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) { :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