# 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>
````
