strapi支持的富文本编辑器是 markdown,那 nuxt 就涉及到 markdown 的解析了,关于markdown的解析我们选用行业比较推荐的库markdown-it

安装并应用

yarn add markdown-it
yarn add @types/markdown-it -D

通过插件的方式引入 markdown-it,并对 markdown-it 配置对应的代码高亮,图片懒加载及资源链接增加CDN前缀,目录生成等功能。

// plugins/markdown.ts
import markdownIt from "markdown-it"
import Token from "markdown-it/lib/token";

const markdown = markdownIt({
	html: true,
	typographer: true,
	highlight: function (str: string, lang: string) {
		if (lang === 'mermaid') { // 配合 mermaid 库让 markdown 支持流程图绘制
			return `<pre class="mermaid">${str}</pre><script></script>`;
		} else {
			// 处理html代码块被转义导致代码高亮的问题
			str = str.replace(/<|>|&/g, (matches) => {
				return ({
					'<': '&lt;',
					'>': '&gt;',
					'&': '&amp;'
				})[matches] || "";
			});
			return `<pre class="code-area line-numbers"><code class="${lang ? 'language-' + lang : ''}">${str}</code></pre>`
		}
	}
})

markdown.renderer.rules.image = function (tokens, idx, options, env, self) {
	let src = tokens[idx].attrGet("src");
	if (src) {// 为图片增加懒加载标识
		tokens[idx].attrSet("alt", tokens[idx].content)
		tokens[idx].attrSet("title", tokens[idx].content)
		tokens[idx].attrSet("src", "/img/placeholder.svg")
		if (src.indexOf("http") === -1) {// 当图片链接为相对路径时则对链接增加前缀与图片处理参数
			src = mediaUrl(src || "")
		}
		tokens[idx].attrSet("data-src", src)
		tokens[idx].attrSet("loading", "lazy")
	}
	return self.renderToken(tokens, idx, options)
};

markdown.renderer.rules.link_open = function (tokens, idx, options, env, self) {
	// 判断链接是否包含http,如果有则表示为外链,增加target="_blank" 与 ref="noopener nofollow" 标识
	let href = tokens[idx].attrGet('href') || ""
	if (tokens[idx].attrIndex("target") < 0 && href.indexOf("http") > -1) {
		tokens[idx].attrPush(["target", "_blank"])
		tokens[idx].attrPush(["ref", "noopener nofollow"])
	}
	return self.renderToken(tokens, idx, options)
};

// #region 目录功能
function uuid() { //生成随机uuid
	return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : r & 0x3 | 0x8; return v.toString(16); });
}

interface IToc {
	id: string;
	title: string;
	level: number;
}
markdown.core.ruler.push("toc", (state) => {
	if (state.env?.toc) { // 通过env删除需要目录
		// 遍历所有标题做目录
		let tocArr = state.tokens.reduce((arr: IToc[], token, idx) => {
			if (token.type == "heading_open" && state.tokens[idx + 1] && state.tokens[idx + 1].type === "inline") {
				let inline = state.tokens[idx + 1];
				let level = token.tag.replace("h", "")
				token.attrPush(["level", level])
				let anchorName = encodeURIComponent(inline.children?.reduce((acc, token) => acc + token.type == "text" ? token.content : "", "") || uuid())
				// #region 标题前增加符号 # 索引
				let anchorLink = [new Token("link_open", "a", 1), new Token("text", "a", 1), new Token("link_open", "a", -1)]
				anchorLink[0].attrPush(["href", `#${anchorName}`])
				anchorLink[1].content = "#"
				inline.children?.push(...anchorLink)
				// #endregion
				token.attrPush(["id", anchorName])
				arr.push({
					id: anchorName,
					title: inline.content,
					level: parseInt(level)
				})
			}
			return arr;
		}, [])
		if (tocArr.length) {
			let baseLevel = tocArr[0].level;
			let tocMarkdownStr = tocArr.reduce((mdStr, toc) => {
				let level = toc.level - baseLevel;
				level = level < 0 ? 0 : level;
				mdStr += `${new Array(level).fill("").map(() => '\t').join("")}- [${toc.title}](#${toc.id})\n`
				return mdStr
			}, '')
			let tocTokens = markdown.parse(tocMarkdownStr, {})
			// 利用markdown生成目录列表渲染成Html并包裹。
			if (tocTokens.length) {
				let tocWrapStart = new Token("html_block", "", 0)
				tocWrapStart.content = `<div class="article-toc"><div class="article-toc-panel"><strong>目录</strong>`
				let tocWrapEnd = new Token("html_block", "", 0)
				tocWrapEnd.content = `</div></div>`
				tocTokens.unshift(tocWrapStart)
				tocTokens.push(tocWrapEnd)
				state.tokens = tocTokens.concat(state.tokens)
			}
		}
	}

})
// #endregion


// 向nuxt全局注入 $md 方法调用此 markdown 实例
export default defineNuxtPlugin(async (nuxtApp) => {
	nuxtApp.provide("md", markdown) 
})

Markdown实例的应用

<!-- post/[...slug].vue -->
<template>
  <article>
    <div class="post-content" v-html="$md.render(文章内容, {toc: true})"></div>
  </article>
</template>
<script lang="ts" setup>
import { lazyLoad } from "unlazy"
// 通过CDN引入prism.js代码高亮库与mermaid@10.3.1的流程图绘制库。
useHead({
	link: [
		{ rel: "stylesheet", href: "https://cdn.bootcdn.net/ajax/libs/prism/1.29.0/themes/prism.min.css" },
		{ rel: "stylesheet", href: "https://cdn.bootcdn.net/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.css" },
	],
	script: [
		{ src: "https://cdn.bootcdn.net/ajax/libs/prism/1.29.0/components/prism-core.min.js", async: true },
		{ src: "https://cdn.bootcdn.net/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js", async: true },
		{ src: "https://cdn.bootcdn.net/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.js", async: true },
		{ src: "https://cdn.bootcdn.net/ajax/libs/mermaid/10.3.1/mermaid.min.js" },
	]
})
  
onMounted(() => {
	window.Prism?.highlightAll()
	window.mermaid?.run()
	lazyLoad();
})
</script>