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 ({
'<': '<',
'>': '>',
'&': '&'
})[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>