一、为什么使用Marked.js
marked.js 用于将 markdown 格式字符串转换为对应的带有 html 标签包裹的字符串。通常适用于 AI 产品的聊天窗口,将模型返回的 markdown 格式的字符串转换成对应格式的 html 标签,在聊天窗口中显示。
二、marked.js 的使用方法
1,安装 marked.js
npm i marked / yarn add marked / pnpm install marked
2,使用 marked.js
javascript
<template>
<p v-html="marked(message)"><p/>
<template/>
<script setup>
import { ref} from 'vue'
import {marked} from 'marked'
const message = ref()
<script/>
三、配置换行
因为 marked.js 直接使用是默认不换行的,所以如果想要使 marked.js 的换行效果同后端返回的字符串样式对齐的话,需要对其进行配置 breaks : true ;
javascript
marked.setOptions({
gfm: true, // 使用 GitHub Flavored Markdown (GFM)
breaks: true, // 转换换行符为 <br> 标签
pedantic: false, // 不遵守原始的 Markdown 标准
sanitize: false, // 不对输出进行清理/转义(默认情况下会转义 HTML)
smartLists: true, // 使用智能列表识别(例如正确的缩进)
smartypants: false // 不使用智能引号处理
});
四、重写 p 标签渲染事件, 解决空行丢失问题
如果后端传回的字符串后面带连续的两个\n,在转为 markdown 的时候是需要出现一个空行的,但是 marked.js 会把 \n\n 后面的一段文本转换成 p 标签,所以,空行会消失。所以需要重写 p 标签的渲染事件,paragraph 在 p 标签前面增加一个换行 br 标签
javascript
import { marked, Renderer} from 'marked';
// 重写marked 渲染逻辑
const renderer = new Renderer();
marked.setOptions({
gfm: true,
smartLists: true,
renderer: renderer,
breaks: true,
});
renderer.paragraph = (data) => {
return "</br>" + "<p>" + marked.parseInline(data.raw) + "</p>";
};
五、重写 li 标签渲染事件, 解决转换有序列表字符串为 li 时,序号丢失问题。
当 marked.js 转换字符串中的有序列表时,会造成序号丢失问题。
例如:1, aaaaaaaaaa。/n 2,bbbbbbbbbb。
marked.js 会将其转换成 <li>aaaaaaaaaa。<li> <li>bbbbbbbbbb。<li/> 。 造成 首位数字的丢失
这里是指 从 1 开始的有序列表转换时会造成 数字的丢失,但是不是从 1 开始的并不会丢失序号数字。
可以通过改写 li 标签的渲染事件 listitem。解决该问题。
javascript
// 重写marked 的有序列表,子元素 li 渲染逻辑
renderer.listitem = (data) => {
// 普通列表项添加自定义 class
let liTextList;
liTextList = marked.parseInline(data.raw).split("<br>") as string[];
// 根据每个字符串中开始的空格缩进来分组用li 包裹
if (liTextList.length > 1) {
return liTextList.reduce((prev, curr) => {
const spaceLength = curr.match(/^\s*/)?.[0].length;
// 首行有空格,有缩进,并不代表是有 有序列表或者无序列表
if (spaceLength) {
// 计算当前字符串的缩进层级
const level = Math.floor(spaceLength / 2);
let resultHtmlString = prev + "<li";
// 此时才是有有序列表或者无序列表的情况
if (/^ +\* +/.test(curr)) {
resultHtmlString += ` class='li-level-${level}'>`;
curr = curr.replace(/^ +\* +/, "");
} else {
resultHtmlString += `>`;
}
return resultHtmlString + curr + "</li>";
} else if (/^\* +/.test(curr)) {
curr = curr.replace(/^\* */, "");
// 以* 开头,没有缩进,直接是黑色圆点无序列表
return prev + `<li class="li-level-0">${curr}</li>`;
} else {
return prev + `<li>${curr}</li>`;
}
}, "");
} else if (liTextList[0] && /^\* +/.test(liTextList[0])) {
data.raw = data.raw.replace(/^\* +/, "");
// 以* 开头,没有缩进,直接是黑色圆点无序列表
return "<li class='li-level-0'>" + marked.parseInline(data.raw) + "</li>";
} else {
// 只有一行的情况下,直接渲染
return "<li>" + marked.parseInline(data.raw) + "</li>";
}
};
再根据 level 层级修改对应的 li 标签的样式属性,增加不同的缩进,以及 list-style-type
对于无序列表ul 来说,内层嵌套 li 层级的 list-style-type 的顺序是 disc:黑色小圆点。circle:白色小圆点。square:黑色小方块。依次循环
css
.li-level-0 {
display: list-item;
margin-left: 30px;
list-style-position: outside;
list-style-type: disc;
}
.li-level-2 {
display: list-item;
margin-left: 60px;
list-style-position: outside;
list-style-type: circle;
}
.li-level-4 {
display: list-item;
margin-left: 90px;
list-style-position: outside;
list-style-type: square;
}