Vue3实现渲染markdown内容且代码块高亮

203 阅读1分钟
  1. 安装依赖
npm install marked
npm install marked-highlight
npm install github-markdown-css
npm install highlight.js
npm install dompurify
  1. markdown组件
<template>
  <div class="markdown-body" v-html="sanitizedHtml" />
  <svg class="circular" viewBox="0 0 50 50" v-if="markLoad">
    <circle class="path" cx="25" cy="25" r="20" fill="none"></circle>
  </svg>
</template>

<script lang="ts" setup>
import { computed } from 'vue'
import DOMPurify from 'dompurify'
import { Marked } from 'marked'
import { markedHighlight } from 'marked-highlight'
import hljs from 'highlight.js'
import 'highlight.js/styles/atom-one-dark.css'
import 'github-markdown-css'

// 单例 Marked 配置
const marked = new Marked(
  markedHighlight({
    langPrefix: 'hljs language-',
    highlight(code, lang) {
      const validLang = hljs.getLanguage(lang) ? lang : 'plaintext'
      try {
        return hljs.highlight(code, { language: validLang }).value
      } catch {
        return hljs.highlightAuto(code).value
      }
    }
  })
)

// Props 接收内容
const props = defineProps({
  content: { type: String, required: true, default: "", },
  markLoad: { type: Boolean, default: false }
})
// 安全处理流程
const sanitizedHtml = computed(() => {
  const rawHtml = marked.parse(props.content) as string
  return DOMPurify.sanitize(rawHtml, {
    ADD_ATTR: ['target'] // 允许链接 target 属性
  })
})
// 链接安全增强
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
  if (node.tagName === 'A') {
    node.setAttribute('rel', 'noopener noreferrer')
  }
})
</script>

<style lang="scss">
.markdown-body {

  ul,
  ol {
    list-style: revert !important;

    li {
      list-style: inherit !important;
    }
  }

  table {
    width: auto;

    th,
    td {
      border: 1px solid #ddd;
      padding: 8px 12px;
    }

    tr:nth-child(even) {
      background: #f8f8f8;
    }
  }
}
</style>
<style lang="scss" scoped>
.circular {
  display: inline;
  height: 24px;
  width: 24px;
  margin-top: 4px;
  animation: loading-rotate 2s linear infinite
}

.path {
  animation: loading-dash 1.5s ease-in-out infinite;
  stroke-dasharray: 90, 150;
  stroke-dashoffset: 0;
  stroke-width: 2;
  stroke: var(--el-color-primary);
  stroke-linecap: round
}

@keyframes loading-rotate {
  to {
    transform: rotate(360deg)
  }
}

@keyframes loading-dash {
  0% {
    stroke-dasharray: 1, 200;
    stroke-dashoffset: 0
  }

  50% {
    stroke-dasharray: 90, 150;
    stroke-dashoffset: -40px
  }

  to {
    stroke-dasharray: 90, 150;
    stroke-dashoffset: -120px
  }
}
</style>
  1. 父组件
<markdown-content :content="chatContent.msgcontent" :markLoad="chatContent.loading"></markdown-content>