Vue3集成MarkDown格式+MathJax数学公式展示(保准无坑)

前言

最近写了一个AI对话,接入了DeepSeek接口,但是苦于无法将AI回复的内容进行MarkDown格式化,虽然之前也做过一个UniApp版的,用的是ua-markdown插件,这次做的是Web版的,想换个插件试试,网上查了好多,真正能实现的并不多,所有我也整理一份,帮助大家快速实现。

一、功能概述与准备工作

核心功能实现目标

  1. 动态渲染Markdown内容:将Markdown文本转换为HTML并正确渲染
  2. 代码块高亮:自动识别多种编程语言并应用语法高亮
  3. LaTeX数学公式渲染:支持内联和块级数学公式展示
  4. 一键复制代码:为用户提供便捷的代码复制功能

我这里就直接用我现在做的Vue3项目,大家可以根据自己项目进行调整

二、依赖安装与配置

1. 安装必要依赖

npm install marked marked-highlight github-markdown-css highlight.js element-plus
npm i less -D

2. 调整MathJax的引用方式

因为mathjax不支持import方式引入,所以需要将MathJax的文件从node_modules目录下拷贝到public目录中
在这里插入图片描述

3. 自定义MathJax配置,解析数学公式

在src/utils文件夹下创一个文件MathJax.ts文件,编写如下代码:
这里我用的是TypeScript,如果是JavaScript自行将ts转为js

// Declare MathJax as a global property on the Window interface
// This is TypeScript syntax to extend the global Window type
declare global {
  interface Window {
    MathJax: any;  // Declare that window.MathJax exists (type 'any' for simplicity)
  }
}

// Configure MathJax settings by assigning to window.MathJax
window.MathJax = {
  // TeX input processor configuration
  tex: {
    // Define delimiters for inline math expressions:
    inlineMath: [
      ["$", "$"],    // $...$ for inline math
      ["\\(", "\\)"] // \(...\) alternative syntax
    ],
    // Define delimiters for display (block) math expressions:
    displayMath: [
      ["$$", "$$"],  // $$...$$ for block math
      ["\\[", "\\]"] // \[...\] alternative syntax
    ]
  },
  // General processing options
  options: {
    // HTML tags that MathJax should skip/ignore
    skipHtmlTags: ["script", "noscript", "style", "textarea", "pre", "code"],
    // CSS class that tells MathJax to ignore elements
    ignoreHtmlClass: "tex2jax_ignore",
    // CSS class that tells MathJax to explicitly process elements
    processHtmlClass: "tex2jax_process",
  }
};

// Export the MathJax configuration (though it's already globally available)
export default window.MathJax;

这里还没完,还需要再main.ts中引入mathJax和上面组件

import "@/utils/MathJax"; // 必须在引入mathjax前引入mathjax的配置文件
import "mathjax/es5/tex-svg"; // 引入 tex-svg.js 样式

4. 创建渲染组件MarkDown.vue

这里的MarkDown.vue主要是将html渲染成markdown格式,代码如下:

<template>
    <div v-html="htmlContent" class="markdown-body"></div>
</template>

<script lang="ts" setup>
import { nextTick, onMounted, ref, watch, defineProps} from 'vue';
import { Marked } from 'marked';
import { markedHighlight } from 'marked-highlight';
import hljs from 'highlight.js';
import 'highlight.js/styles/github.css';
import { ElMessage } from 'element-plus';
import 'github-markdown-css/github-markdown-light.css';

const props = defineProps<{ value: string }>();

const htmlContent = ref('');

let doCopy = (e: Event) => {
    const code = (e.currentTarget as HTMLElement).parentNode?.parentNode?.querySelector('code');
    if (code) {
        navigator
        .clipboard
        .writeText((code as HTMLElement).innerText)
        .then(() => {
            ElMessage.success('复制成功');
        })
        .catch(() => {
            ElMessage.error('复制失败');
        });
    }
};

const marked = new Marked(
    markedHighlight({
        async: false,
        langPrefix: 'language-',
        emptyLangClass: 'no-lang',
        highlight: (code, language) => {
            return hljs.highlightAuto(code, [language]).value;
        },
    })
);

const enhanceCodeBlock = (content: any) => {
    // 直接在<pre>内插入复制按钮,确保唯一性和样式优先级
    return content.replace(/<pre><code/g, `<pre><div class="enhance"><div class="copyCode">复制</div></div><code`);
};

const bindCopyFunction = (el: Element) => {
    const codeBlocks = el.querySelectorAll('pre');
    // console.log("codeBlocks = ", codeBlocks);
    codeBlocks.forEach((codeBlock) => {
        const enhance = codeBlock.querySelector('.enhance');
        // console.log("enhance = ", enhance);
        if (enhance) {
            const copyCode = enhance.querySelector('.copyCode');
            // console.log("copyCode = ", copyCode);
            if (copyCode) {
                copyCode.removeEventListener('click', doCopy);
                copyCode.addEventListener('click', doCopy);
            }
        }
    });
};
let globalMathIdx = 0;
// 替换数学公式的标识符,以防marked.js的渲染将数学公式改变,导致MathJax无法正常渲染数学公式
const processEscapes = (raw: string) => {
    const mathMap = new Map();
    // 获取时间戳
    const timestamp = new Date().getTime();
    // 匹配 $$...$$
    raw = raw.replace(/\$\$([\s\S]+?)\$\$/g, (m) => {
        const key = `MATH_BLOCK_${timestamp}_${globalMathIdx++}__TRANSTRATION`;
        mathMap.set(key, m);
        return key;
    });
    // 匹配 \[...\]
    raw = raw.replace(/\\\[([\s\S]+?)\\\]/g, (m) => {
        const key = `MATH_BLOCK_${timestamp}_${globalMathIdx++}__TRANSTRATION`;
        mathMap.set(key, m);
        return key;
    });
    // 匹配 $...$
    raw = raw.replace(/\$(.+?)\$/g, (m) => {
        const key = `MATH_INLINE_${timestamp}_${globalMathIdx++}__TRANSTRATION`;
        mathMap.set(key, m);
        return key;
    });
    // 匹配 \\(...\\)
    raw = raw.replace(/\\\((.+?)\\\)/g, (m) => {
        const key = `MATH_INLINE_${timestamp}_${globalMathIdx++}__TRANSTRATION`;
        mathMap.set(key, m);
        return key;
    });
    return { raw, mathMap };
}
// 还原数学公式的标识符,在marked渲染后执行
// 将前面替换的数学公式标识符替换回来,然后再由MarkJax渲染数学公式
const restoreEscapes = (raw: string, mathMap: Map<string, string>) => {
    mathMap.forEach((val, key) => {
        raw = raw.replaceAll(key, val);
    });
    return raw;
}
const parseMarkdown = () => {
    // 数学公式占位符处理
    // 后面marked解析后再替换回来
    const { raw, mathMap } = processEscapes(props.value);
    htmlContent.value = raw;
    nextTick(() => {
        const el = document.querySelectorAll('.markdown-body');
        const item = el[el.length - 1];
        // marked 解析
        let html: any = marked.parse(raw) || '';
        // 还原数学公式
        html = restoreEscapes(html, mathMap);
        htmlContent.value = enhanceCodeBlock(html);
        nextTick(() => {
            if (el) {
                if (window.MathJax && window.MathJax.typesetPromise) {
                    window.MathJax.typesetPromise([item]);
                }
                bindCopyFunction(item);
            }
        });
    });
};

watch(
    () => props.value,
    () => {
        parseMarkdown();
    },
    { immediate: true }
);

onMounted(() => {
    if (props.value) {
        parseMarkdown();
    }
});
</script>
<style lang="less">
.markdown-body {
    padding: 0 10px;
    box-sizing: border-box;
}

pre {
    position: relative;
}

pre .enhance {
    display: flex;
    color: #247aaa;
    padding: 5px 5px 0 0;
    box-sizing: border-box;
    font-size: 12px;
    border-radius: 9px;
    justify-content: flex-end;
    align-items: flex-start;
    position: absolute;
    top: 0;
    right: 0;
    height: 36px;
    z-index: 2;
    background: transparent;
}

pre .enhance .copyCode {
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    background: #fff;
    border-radius: 6px;
    padding: 2px 10px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.04);
    margin-top: 4px;
    margin-right: 4px;
    transition: color 0.2s;
    font-weight: bold;
    &:hover {
        color: rgba(2, 120, 255, 0.84);
        background: #f0f7ff;
    }
    i {
        font-size: 16px;
        margin-left: 5px;
    }
}

.markdown-body code,
.markdown-body tt {
    background-color: #ffe6e6;
    color: #df3b3b;
}
</style>

三、测试与运行

前面已经把基本的配置完成了,接下来就应用到我的项目中,然后直接上效果图
这里就直接安装如下操作引入MarkDown组件,value的参数填写需要转换的html文本即可

<template>
	<div>
		<MarkDown :value="content"></MarkDown>
	</div>
</template>
<script lang="ts" setup>
import {nextTick, onMounted, ref} from 'vue';
import MarkDown from "@/components/MarkDown.vue";
const content = ref('替换成需要解析的文本');
</script>

在这里插入图片描述

总结

通过这套方案,你可以构建出功能丰富、用户体验良好的Markdown展示界面,支持代码块高亮和LaTex数学公式渲染,并提供代码复制等功能。希望本片文章对你有所帮助,谢谢观看!如果有任何问题,欢迎随时向我询问。

参考博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值