UNPKG

vue-markdown-mini

Version:

- 一个基于Vue的Markdown渲染组件,支持实时预览、主题切换和自定义扩展,适用于快速构建内容丰富的Web应用。

1,023 lines (870 loc) 29.3 kB
# vue-markdown-mini - 一个基于Vue的Markdown渲染组件,支持实时预览、主题切换和自定义扩展,适用于快速构建内容丰富的Web应用。 ## install - npm install vue-markdown-mini mermaid --save-dev ### 参数 | 参数名 | 类型 | 默认值 | 说明 | | -------------- | --------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | content | `string` | '' | MarkDown文本 | | theme | [`Theme`](#theme) | 'light' | MarkDown卡片主题 | | mdOptions | `object` | {} | 设置 markdown 对字符串的处理方式, 可参考[markdown-it](https://www.npmjs.com/package/markdown-it?activeTab=readme) | | mdPlugins | [MdPlugin[]](#mdplugin) | [] | 设置 markdown-it 插件 | | customXssRules | [CustomXssRule[]](#customxssrule) | [] | 自定义 xss 对某种 tag 的过滤方式,每条规则需要指定 tag, 并给出需要加入白名单的属性数组 | | enableThink | `boolean` | false | 是否开启\<think\>标签识别 | | thinkOptions | [`ThinkOptions`](#thinkoptions) | -- | \<think\>标签配置,自定义样式等 | | typing | `boolean` | false | 开启打字机效果 | | typingOptions | [`TypingOptions`](#typingoptions) | `{ step: 2, interval: 50, style: 'normal' }` | 打字机效果配置 | | renderMermaid | `boolean` | false | 开启mermaid渲染图表 | ### 代码块插槽 | 插槽名 | 返回值 | 说明 | | ----------- | -------------------------------------------------- | ---------------------------- | | codeActions | { codeBlockData: [CodeBlockData](#codeblockdata) } | 代码块头部右侧自定义操作区域 | | codeHeader | { codeBlockData: [CodeBlockData](#codeblockdata) } | 自定义代码块头部区域 | | codeContent | { codeBlockData: [CodeBlockData](#codeblockdata) } | 自定义代码块内容区域 | ### 事件 | 事件名 | 返回值 | 说明 | | -------------- | ------ | -------------------------------------- | | after-mdt-init | Object | markdown-it实例mdt,在初始化完成后返回 | | typingStart | -- | 打字效果开始回调 | | typing | -- | 打字中每一步回调 | | typingEnd | -- | 打字结束回调 | ### 类型定义 #### CustomXssRule ```ts interface CustomXssRule { key: string value: string[] | null } ``` #### Mdplugin ```ts interface Mdplugin { plugin: any opts?: Object } ``` #### Theme ```ts type Theme = 'light' | 'dark' ``` #### CodeBlockData ```ts interface CodeBlockData { code: string language: string } ``` #### ThinkOptions ```ts interface ThinkOptions { customClass: string } ``` #### TypingOptions ```ts interface TypingOptions { step: number | [number, number] // 可指定两个数字之间step interval: number style: 'normal' | 'cursor' | 'gradient' | 'color' } ``` ## 使用方法 - 引入组件及样式 - import VueMarkdownMini from 'vue-markdown-mini' - import 'vue-markdown-mini/scss/index.scss' - app.component('VueMarkdownMini',VueMarkdownMini) ### 基本用法 基本用法只需传入 content 即可 :::demo ```vue <template> <VueMarkdownMini :content="content" :theme="theme"></VueMarkdownMini> </template> <script setup> import { ref, onMounted } from 'vue' let themeService const theme = ref('light') const content = ref(` # 快速排序(Quick Sort) ### 介绍 **快速排序(Quick Sort)**:是一种高效的排序算法,它采用分治法(Divide and Conquer)的思想。它的基本思路是: 1. 选择一个基准值(pivot) 2. 将数组分成两部分:小于基准值的部分和大于等于基准值的部分 3. 递归地对这两部分进行排序 ### 代码实现 1. 以下是快速排序的实现方法 \`\`\`ts function quickSort(arr) { function quickSort(arr) { if (arr.length < 2) { return arr; } const pivot = arr[0]; const left = []; const right = []; for (let i = 1; i < arr.length; i++) { if (arr[i] < pivot) { left.push(arr[i]); } else { right.push(arr[i]); } } return [...quickSort(left), pivot, ...quickSort(right)]; } // 使用示例 const arr = [3, 6, 8, 10, 1, 2, 1]; console.log(quickSort(arr)); // 输出排序后的数组 } \`\`\` `) const changeTheme = () => { theme.value = theme.value === 'light' ? 'dark' : 'light' themeClass.value = themeClass.value === 'light-background' ? 'dark-background' : 'light-background' } const themeChange = () => { if (themeService) { theme.value = themeService.currentTheme.id === 'infinity-theme' ? 'light' : 'dark' } } onMounted(() => { if (typeof window !== 'undefined') { themeService = window['devuiThemeService'] } themeChange() if (themeService && themeService.eventBus) { themeService.eventBus.add('themeChanged', themeChange) } }) </script> ``` ::: ### 打字机效果 支持配置打字机动效果,当前内置不同效果样式可配置,支持配置动效速度与打字间隔,也适用流式数据返回场景。 :::demo ```vue <template> <div class="btn-container"> <d-button variant="solid" @click="generateAnswer">重新执行</d-button> </div> <div> <span class="demo-title">默认效果</span> <McBubble :variant="'bordered'"> <VueMarkdownMini :content="content" :theme="theme" :typing="true"></VueMarkdownMini> </McBubble> <span class="demo-title">打字机并配置打字速度</span> <McBubble :variant="'bordered'"> <VueMarkdownMini :content="content" :theme="theme" :typing="true" :typingOptions="typingOptions1" ></VueMarkdownMini> </McBubble> <span class="demo-title">渐变打字</span> <McBubble :variant="'bordered'"> <VueMarkdownMini :content="content" :theme="theme" :typing="true" :typingOptions="typingOptions2" ></VueMarkdownMini> </McBubble> <span class="demo-title">彩色打字</span> <McBubble :variant="'bordered'"> <VueMarkdownMini :content="content" :theme="theme" :typing="true" :typingOptions="typingOptions3" ></VueMarkdownMini> </McBubble> <span class="demo-title">流式返回</span> <McBubble :variant="'bordered'"> <VueMarkdownMini :content="content1" :theme="theme" :typing="true" :typingOptions="typingOptions4" @typingEnd="typingEnd" ></VueMarkdownMini> </McBubble> </div> </template> <script setup> import { ref, onMounted } from 'vue' const MOCK_CONTENT = `**我了解到了你的需求**,*会进行<span style="color:red">打字机效果输出</span>,如果你需要重新执行打字机动效*,可点击重新执行按钮。` let themeService const content = ref(MOCK_CONTENT) const theme = ref('light') const typingOptions1 = { step: [1, 5], interval: 200, style: 'cursor', } const typingOptions2 = { interval: 200, style: 'gradient', } const typingOptions3 = { interval: 200, style: 'color', } const typingOptions4 = { interval: 200, step: 1, } const content1 = ref('') let interval let stramEnd = false const streamContent = () => { clearInterval(interval) let currentIndex = 0 interval = setInterval(() => { currentIndex += Math.ceil(Math.random() * 10) content1.value = MOCK_CONTENT.slice(0, currentIndex) if (currentIndex > MOCK_CONTENT.length) { stramEnd = true clearInterval(interval) } }, 100) } const generateAnswer = () => { content.value = '' content1.value = '' setTimeout(() => { content.value = MOCK_CONTENT streamContent() }) } const typingEnd = () => { if (stramEnd) { console.log('流式与打字机效果完成') } } const themeChange = () => { if (themeService) { theme.value = themeService.currentTheme.id === 'infinity-theme' ? 'light' : 'dark' } } onMounted(() => { streamContent() if (typeof window !== 'undefined') { themeService = window['devuiThemeService'] } themeChange() if (themeService && themeService.eventBus) { themeService.eventBus.add('themeChanged', themeChange) } }) </script> <style lang="scss" scoped> .btn-container { display: flex; margin-bottom: 12px; } .demo-title { margin: 20px 0px 8px 8px; display: inline-block; } </style> ``` ::: ### think标签支持 支持自定义的 think 标签,用于包裹特定内容并渲染为自定义样式的块级元素。适合用于强调思考过程或特殊内容展示 :::demo ```vue <template> <div class="btn-container"> <d-button variant="solid" @click="generateAnswer">{{ isLoading ? '停止' : '重新生成' }}</d-button> </div> <div> <template v-for="(msg, idx) in messages" :key="idx"> <McBubble v-if="msg.from === 'user'" :content="msg.content" :align="'right'" :avatarConfig="msg.avatarConfig" ></McBubble> <McBubble v-else :loading="msg.loading ?? false" :avatarConfig="msg.avatarConfig" :variant="'bordered'" > <div class="think-toggle-btn" @click="toggleThink(idx)"> <i class="icon-point"></i> <span>{{ thinkBtnText }}</span> <i :class="btnIcon"></i> </div> <VueMarkdownMini :enableThink="true" :content="msg.content" :theme="theme" ></VueMarkdownMini> </McBubble> </template> </div> </template> <script setup> import { ref, onMounted } from 'vue' let themeService const theme = ref('light') const messages = ref([]) const aiModelAvatar = { imgSrc: 'https://matechat.gitcode.com/logo.svg', width: 32, height: 32, } const isLoading = ref(false) let interval = null const thinkBtnText = ref('已深度思考 (用时16秒)') const btnIcon = ref('icon-chevron-up-2') const mockAnswer = ` <think> 嗯,用户让我帮他写一个快速排序。我得先回想一下快速排序的原理。快速排序是分治算法的一种,基本步骤是选一个基准元素,然后把数组分成两部分,一部分比基准小,另一部分比基准大,然后递归地对这两部分排序。 首先,我得确定基准的选择。通常有三种方法:选第一个元素、选中间元素或者随机选。选中间可能比较好,避免最坏情况的时间复杂度。比如数组已经有序的时候,选第一个会导致O(n²)的时间复杂度,但选中间元素可能更平衡。 然后,分区操作怎么做呢?分区的目标是把比基准小的放左边,大的放右边。可以用双指针的方法,左边找比基准大的,右边找比基准小的,然后交换,直到指针相遇。这时候再把基准放到正确的位置,返回基准的索引。 接下来是递归调用。对基准左边的子数组和右边的子数组分别进行快速排序,直到子数组的长度小于等于1,这时候数组就已经有序了。 现在要考虑用户的需求。用户可能对编程不太熟悉,所以代码需要简洁易懂,同时要有注释说明。或者用户可能需要一个高效的实现,不过快速排序的平均时间复杂度已经是O(n log n)了,应该没问题。 可能的边缘情况是数组为空或只有一个元素,这时候直接返回。所以函数里要先处理这种情况。 然后,测试一下这个函数是否正确。比如输入一个无序数组,比如[3,6,8,10,1,2,1],看看排序后的结果是否是[1,1,2,3,6,8,10]。或者测试已经有序的数组,比如[1,2,3,4,5],确保不会出现栈溢出或者错误的结果。 还有,是否需要处理重复元素?快速排序本身是能处理重复的,所以应该没问题。 再考虑语言的问题,用户没有说明用什么语言,但根据用户的名字或者上下文,可能想要Python的实现?或者可能其他语言。不过根据常见情况,可能先给出Python版本。 所以,综合这些思考,可以写出一个Python的快速排序函数。基准选中间元素,分区使用双指针,递归处理左右子数组。然后加上注释,解释每一步的作用。 最后,给出示例用法,让用户知道如何调用这个函数,并验证结果是否正确。 </think> 以下是使用 Python 实现的快速排序算法,包含详细注释: \`\`\`python def quick_sort(arr): """实现快速排序的递归函数""" # 基线条件:当数组长度小于等于1时,直接返回 if len(arr) <= 1: return arr # 选择中间元素作为基准值(可有效避免最坏情况) pivot = arr[len(arr) // 2] # 将数组分成三部分:小于基准、等于基准、大于基准 left = [x for x in arr if x < pivot] middle = [x for x in arr if x == pivot] right = [x for x in arr if x > pivot] # 递归排序左右子数组,合并结果 return quick_sort(left) + middle + quick_sort(right) # 示例用法 if __name__ == "__main__": example_arr = [3,6,8,10,1,2,1] sorted_arr = quick_sort(example_arr) print("原数组:", example_arr) print("排序后:", sorted_arr) \`\`\` ` const changeTheme = () => { theme.value = theme.value === 'light' ? 'dark' : 'light' themeClass.value = themeClass.value === 'light-background' ? 'dark-background' : 'light-background' } const themeChange = () => { if (themeService) { theme.value = themeService.currentTheme.id === 'infinity-theme' ? 'light' : 'dark' } } const toggleThink = (idx) => { if (isLoading.value) { return } const targetNode = document.querySelectorAll('.mc-bubble-content-container')[idx] if (targetNode) { const thinkBlock = targetNode.querySelector('.mc-think-block') if (thinkBlock) { const currentDisplay = getComputedStyle(thinkBlock).display thinkBlock.style.display = currentDisplay === 'none' ? 'block' : 'none' btnIcon.value = currentDisplay === 'none' ? 'icon-chevron-up-2' : 'icon-chevron-down-2' } } } const generateAnswer = () => { if (isLoading.value) { isLoading.value = false clearInterval(interval) } else { isLoading.value = true messages.value = [ { from: 'ai-model', avatarConfig: { ...aiModelAvatar }, content: '', loading: false, }, ] thinkBtnText.value = '思考中...' let currentIndex = 0 let totalTime = 0 interval = setInterval(() => { if (currentIndex < mockAnswer.length) { messages.value[messages.value.length - 1].content = mockAnswer.slice(0, currentIndex) currentIndex += 10 totalTime += 100 if ( messages.value[messages.value.length - 1].content.indexOf('</think>') > -1 && thinkBtnText.value === '思考中...' ) { thinkBtnText.value = `已深度思考 (用时${totalTime / 1000}秒)` } } else { isLoading.value = false clearInterval(interval) messages.value[messages.value.length - 1].loading = false } }, 100) } } onMounted(() => { if (typeof window !== 'undefined') { themeService = window['devuiThemeService'] } themeChange() if (themeService && themeService.eventBus) { themeService.eventBus.add('themeChanged', themeChange) } messages.value.push({ from: 'ai-model', avatarConfig: { ...aiModelAvatar }, content: mockAnswer, }) }) </script> ``` ::: ### 主题切换 组件提供了浅色与深色两种主题,默认使用浅色主题,可以通过 theme 属性来切换主题 :::demo ```vue <template> <div class="btn-container"> <d-button variant="solid" @click="changeTheme">切换主题</d-button> </div> <div class="theme-container" :class="themeClass"> <VueMarkdownMini :content="content" :theme="theme"></VueMarkdownMini> </div> </template> <script setup> import { ref, computed, onMounted, onBeforeMount } from 'vue' const theme = ref('light') const themeClass = ref('light-background') let themeService const content = ref(` # 快速排序(Quick Sort) ### 介绍 **快速排序(Quick Sort)**:是一种高效的排序算法,它采用分治法(Divide and Conquer)的思想。它的基本思路是: 1. 选择一个基准值(pivot) 2. 将数组分成两部分:小于基准值的部分和大于等于基准值的部分 3. 递归地对这两部分进行排序 ### 代码实现 1. 以下是快速排序的实现方法 \`\`\`ts function quickSort(arr) { function quickSort(arr) { if (arr.length < 2) { return arr; } const pivot = arr[0]; const left = []; const right = []; for (let i = 1; i < arr.length; i++) { if (arr[i] < pivot) { left.push(arr[i]); } else { right.push(arr[i]); } } return [...quickSort(left), pivot, ...quickSort(right)]; } // 使用示例 const arr = [3, 6, 8, 10, 1, 2, 1]; console.log(quickSort(arr)); // 输出排序后的数组 } \`\`\` `) const changeTheme = () => { theme.value = theme.value === 'light' ? 'dark' : 'light' themeClass.value = themeClass.value === 'light-background' ? 'dark-background' : 'light-background' } const themeChange = () => { if (themeService) { theme.value = themeService.currentTheme.id === 'infinity-theme' ? 'light' : 'dark' themeClass.value = themeService.currentTheme.id === 'infinity-theme' ? 'light-background' : 'dark-background' } } onMounted(() => { if (typeof window !== 'undefined') { themeService = window['devuiThemeService'] } themeChange() if (themeService && themeService.eventBus) { themeService.eventBus.add('themeChanged', themeChange) } }) </script> <style scoped lang="scss"> .btn-container { display: flex; justify-content: end; } .theme-container { margin-top: 8px; padding: 12px; border-radius: 8px; } .light-background { background-color: #f6f6f8; } .dark-background { background-color: #1a1a1c; } </style> ``` ::: ### 数学公式 通过配置md-plugins katex插件,进行数学公式渲染(DEMO未实际渲染,实际使用时解开代码中注释即可按预期渲染)。 :::demo ```vue <template> <VueMarkdownMini :content="content" :theme="theme" :mdPlugins="mdPlugins"></VueMarkdownMini> </template> <script setup> import { ref, onMounted } from 'vue' //import { katex } from '@mdit/plugin-katex'; // 请首先安装@mdit/plugin-katex依赖 let themeService const theme = ref('light') //const mdPlugins = ref([{ // plugin: katex, // opt: {} //}]) const content = ref( ` $E = mc^2$ $\\sqrt{3x-1}+(1+x)^2$ `, ) const handleAction = (codeBlockData) => { console.log(codeBlockData) } const changeTheme = () => { theme.value = theme.value === 'light' ? 'dark' : 'light' themeClass.value = themeClass.value === 'light-background' ? 'dark-background' : 'light-background' } const themeChange = () => { if (themeService) { theme.value = themeService.currentTheme.id === 'infinity-theme' ? 'light' : 'dark' } } onMounted(() => { if (typeof window !== 'undefined') { themeService = window['devuiThemeService'] } themeChange() if (themeService && themeService.eventBus) { themeService.eventBus.add('themeChanged', themeChange) } }) </script> <style> /* @import 'katex/dist/katex.min.css'; 请首先安装 katex 依赖 */ </style> ``` ### PlantUML 渲染 通过配置md-plugins plantuml插件,进行plantuml图渲染。 :::demo ```vue <template> <VueMarkdownMini :content="content" :theme="theme" :mdPlugins="mdPlugins"></VueMarkdownMini> </template> <script setup> import { ref, onMounted } from 'vue' import PlantUml from 'markdown-it-plantuml' // 请首先安装markdown-it-plantuml依赖 let themeService const theme = ref('light') const mdPlugins = ref([ { plugin: PlantUml, opts: { server: 'https://www.plantuml.com/plantuml', }, // 自定义server可参考plantuml官方文档进行搭建 }, ]) const content = ref(` @startuml Alice -> "Bob()" : Hello "Bob()" -> "This is very long" as Long ' You can also declare: ' "Bob()" -> Long as "This is very long" Long --> "Bob()" : ok @enduml`) const handleAction = (codeBlockData) => { console.log(codeBlockData) } const changeTheme = () => { theme.value = theme.value === 'light' ? 'dark' : 'light' themeClass.value = themeClass.value === 'light-background' ? 'dark-background' : 'light-background' } const themeChange = () => { if (themeService) { theme.value = themeService.currentTheme.id === 'infinity-theme' ? 'light' : 'dark' } } onMounted(() => { if (typeof window !== 'undefined') { themeService = window['devuiThemeService'] } themeChange() if (themeService && themeService.eventBus) { themeService.eventBus.add('themeChanged', themeChange) } }) </script> ``` ::: ### emoji渲染 通过配置markdown-it-emoji插件,进行emoji表情渲染。 :::demo ```vue <template> <VueMarkdownMini :content="content" :theme="theme" :mdPlugins="mdPlugins"></VueMarkdownMini> </template> <script setup> import { ref, onMounted } from 'vue' import { full as emoji } from 'markdown-it-emoji' // 请首先安装markdown-it-emoji依赖 let themeService const theme = ref('light') const mdPlugins = ref([ { plugin: emoji, }, ]) const content = ref(` :joy: :thumbsup: :laughing: :blush: :dog: `) const handleAction = (codeBlockData) => { console.log(codeBlockData) } const changeTheme = () => { theme.value = theme.value === 'light' ? 'dark' : 'light' themeClass.value = themeClass.value === 'light-background' ? 'dark-background' : 'light-background' } const themeChange = () => { if (themeService) { theme.value = themeService.currentTheme.id === 'infinity-theme' ? 'light' : 'dark' } } onMounted(() => { if (typeof window !== 'undefined') { themeService = window['devuiThemeService'] } themeChange() if (themeService && themeService.eventBus) { themeService.eventBus.add('themeChanged', themeChange) } }) </script> ``` ::: ### 自定义代码块操作区 我们提供了 `actions` 插槽,支持你自定义代码块操作区 :::demo ```vue <template> <VueMarkdownMini :content="content" :theme="theme"> <template #actions="{ codeBlockData }"> <div class="btn-group"> <d-button variant="solid" @click="handleAction(codeBlockData)">自定义按钮</d-button> </div> </template> </VueMarkdownMini> </template> <script setup> import { ref, onMounted } from 'vue' let themeService const theme = ref('light') const content = ref(`以下是快速排序的实现方法: \`\`\`ts function quickSort(arr) { // 快速排序 } \`\`\` `) const handleAction = (codeBlockData) => { console.log(codeBlockData) } const changeTheme = () => { theme.value = theme.value === 'light' ? 'dark' : 'light' themeClass.value = themeClass.value === 'light-background' ? 'dark-background' : 'light-background' } const themeChange = () => { if (themeService) { theme.value = themeService.currentTheme.id === 'infinity-theme' ? 'light' : 'dark' } } onMounted(() => { if (typeof window !== 'undefined') { themeService = window['devuiThemeService'] } themeChange() if (themeService && themeService.eventBus) { themeService.eventBus.add('themeChanged', themeChange) } }) </script> ``` ::: ### 自定义代码块头部 我们提供了 `header` 插槽,支持你自定义代码块头部区域 :::demo ````vue <template> <VueMarkdownMini :content="content" :theme="theme"> <template #header="{ codeBlockData }"> <div class="header-container"> <div class="header-left"> <img src="https://matechat.gitcode.com/logo.svg" alt="logo" /> <span>MateChat</span> </div> <div class="header-right"> <i class="icon-publish-new"></i> </div> </div> </template> </VueMarkdownMini> </template> <script setup> import { ref, onMounted } from 'vue' let themeService const theme = ref('light') const content = ref(`以下是快速排序的实现方法: \`\`\`ts function quickSort(arr) { // 快速排序 } \`\`\` `) const changeTheme = () => { theme.value = theme.value === 'light' ? 'dark' : 'light' themeClass.value = themeClass.value === 'light-background' ? 'dark-background' : 'light-background' } const themeChange = () => { if (themeService) { theme.value = themeService.currentTheme.id === 'infinity-theme' ? 'light' : 'dark' } } onMounted(() => { if (typeof window !== 'undefined') { themeService = window['devuiThemeService'] } themeChange() if (themeService && themeService.eventBus) { themeService.eventBus.add('themeChanged', themeChange) } }) </script> ::: ### 自定义代码块内容区 我们提供了 `content` 插槽,支持你自定义代码块内容区 :::demo ```vue <template> <VueMarkdownMini :content="content" :theme="theme"> <template #content="{ codeBlockData }"> <div class="content-container" v-html="transfer(codeBlockData)"></div> </template> </VueMarkdownMini> </template> <script setup> import { ref, onMounted } from 'vue' import markdownIt from 'markdown-it' import hljs from 'highlight.js' let themeService const theme = ref('light') const content = ref(`以下是快速排序的实现方法: \`\`\`ts function quickSort(arr) { if (arr.length < 2) { return arr; } const pivot = arr[0]; const left = []; const right = []; for (let i = 1; i < arr.length; i++) { if (arr[i] < pivot) { left.push(arr[i]); } else { right.push(arr[i]); } } return [...quickSort(left), pivot, ...quickSort(right)]; } // 使用示例 const arr = [3, 6, 8, 10, 1, 2, 1]; console.log(quickSort(arr)); // 输出排序后的数组 \`\`\` `) const mdt = markdownIt({ breaks: true, linkify: true, html: true, highlight: (str, lang) => { if (lang && hljs.getLanguage(lang)) { try { const preCode = hljs.highlight(lang, str, true).value const lines = preCode.split(/\n/).slice(0, -1) let html = lines .map((item, index) => { return ( '<li><span class="line-num" data-line="' + (index + 1) + '"></span>' + item + '</li>' ) }) .join('') html = '<ol>' + html + '</ol>' return '<pre class="hljs"><code>' + html + '</code></pre>' } catch (__) {} } const preCode = mdt.utils.escapeHtml(str) const lines = preCode.split(/\n/).slice(0, -1) let html = lines .map((item, index) => { return '<li><span class="line-num" data-line="' + (index + 1) + '"></span>' + item + '</li>' }) .join('') html = '<ol>' + html + '</ol>' return '<pre class="hljs"><code>' + html + '</code></pre>' }, }) const htmlStr = mdt.render(content.value) const transfer = (codeBlockData) => { const { code, language } = codeBlockData const codeBlockStr = '\`\`\`' + language + '\n' + code + '\`\`\`' return mdt.render(codeBlockStr) } const changeTheme = () => { theme.value = theme.value === 'light' ? 'dark' : 'light' themeClass.value = themeClass.value === 'light-background' ? 'dark-background' : 'light-background' } const themeChange = () => { if (themeService) { theme.value = themeService.currentTheme.id === 'infinity-theme' ? 'light' : 'dark' } } onMounted(() => { if (typeof window !== 'undefined') { themeService = window['devuiThemeService'] } themeChange() if (themeService && themeService.eventBus) { themeService.eventBus.add('themeChanged', themeChange) } }) </script> ````