现在写博客基本离不开 Markdown 的处理了,也趁着重构学习一下Markdown大致的处理逻辑博客中常用的 Markdown 处理
安装依赖
bash
yarn add markdown-it
# 需要支持ts时添加
yarn add @types/markdown-it -D
扩展Markdown
多列图片
常常图片截图太长导致,导致文字都被图片挤到不起眼的地方,时而也希望两张图在一行,更好的展示对比情况等。
javascript
import markdownItContainer from "markdown-it-container";
/**
* 让 markdown 支持多列
* 示例:
::: column 列数


:::
*/
markdown.use(markdownItContainer, "column", {
validate: function (params: string) {
return params.trim().match(/^column\s+(.*)$/);
},
render: function (tokens: any, idx: number) {
const m = tokens[idx].info.trim().match(/^column\s+(.*)$/);
if (tokens[idx].nesting === 1) {
// opening tag
return `<div class="column" style="--article-columns: ${markdown.utils.escapeHtml(
m[1]
)}">`;
} else {
// closing tag
return "</div>";
}
},
});
代码折叠及展示
javascript
import copySvg from "~/assets/svg/copy.svg?raw";
import arrowSvg from "~/assets/svg/arrow.svg?raw";
// 对应渲染代码及 mermaid图的方法与事件在 Markdown.vue 执行
markdown.renderer.rules.fence = (tokens, idx) => {
const token = tokens[idx];
const attrs = token.info.trim().split(" ");
const lang = attrs[0] || "plaintext";
const str = token.content;
const title = attrs[1] || "";
if (lang === "mermaid") {
return `<pre class="mermaid">${str}</pre>`;
}
return `
<figure class="code">
<div class="header">
<div class="title">
<span class="collapse-icon" title="折叠代码">${arrowSvg}</span>
${title ? `<b>${title}</b>` : ""}
<span class="lang">${lang}</span>
</div>
<div class="tool">
<span class="copy-icon" title="复制代码">${copySvg}</span>
</div>
</div>
<pre class="line-numbers"><code class="language-${lang}">${markdown.utils.escapeHtml(
str
)}</code></pre>
</figure>`;
};
图片URL处理及增加 lazy
javascript
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");
// env.gallary.push(src)
}
return self.renderToken(tokens, idx, options);
};
外链增加ref
javascript
markdown.renderer.rules.link_open = function (tokens, idx, options, env, self) {
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);
};
目录生成
javascript
function 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);
});
}
markdown.core.ruler.push("toc", (state) => {
if (typeof state.env?.tocCallback === "function") {
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 headerName =
inline.children?.reduce((acc, token) => {
return acc + (token.type == "text" ? token.content : "");
}, "") || "";
let anchorName = encodeURIComponent(headerName || uuid());
token.attrPush(["id", anchorName]);
arr.push({
id: anchorName,
title: headerName,
level: parseInt(level),
children: [],
});
}
return arr;
}, []);
// 将tocArr 按level 变成树级
const tree = tocArr.reduce((acc: IToc[], item: IToc) => {
let currentLevel = acc;
let lastItem = null;
// Traverse the tree to find the correct parent based on the level
for (let i = 1; i < item.level; i++) {
if (lastItem && lastItem.children) {
currentLevel = lastItem.children;
} else {
// If no children array exists, create one
if (lastItem) {
lastItem.children = [];
currentLevel = lastItem.children;
}
}
lastItem = currentLevel[currentLevel.length - 1];
}
// Add the current item to the correct level
currentLevel.push(item);
return acc;
}, []);
state.env.tocCallback.call(undefined, tree);
}
});
最终代码
plugins/markdown.ts
javascript
import markdownIt from "markdown-it";
import copySvg from "~/assets/svg/copy.svg?raw";
import arrowSvg from "~/assets/svg/arrow.svg?raw";
import markdownItContainer from "markdown-it-container";
import type { IToc } from "~/types";
const markdown = markdownIt({
html: true,
typographer: true,
langPrefix: "",
});
markdown.use(markdownItContainer, "column", {
validate: function (params: string) {
return params.trim().match(/^column\s+(.*)$/);
},
render: function (tokens: any, idx: number) {
const m = tokens[idx].info.trim().match(/^column\s+(.*)$/);
if (tokens[idx].nesting === 1) {
// opening tag
return `<div class="column" style="--article-columns: ${markdown.utils.escapeHtml(
m[1]
)}">`;
} else {
// closing tag
return "</div>";
}
},
});
markdown.renderer.rules.fence = (tokens, idx) => {
const token = tokens[idx];
const attrs = token.info.trim().split(" ");
const lang = attrs[0] || "plaintext";
const str = token.content;
const title = attrs[1] || "";
if (lang === "mermaid") {
return `<pre class="mermaid">${str}</pre>`;
}
return `
<figure class="code">
<div class="header">
<div class="title">
<span class="collapse-icon" title="折叠代码">${arrowSvg}</span>
${title ? `<b>${title}</b>` : ""}
<span class="lang">${lang}</span>
</div>
<div class="tool">
<span class="copy-icon" title="复制代码">${copySvg}</span>
</div>
</div>
<pre class="line-numbers"><code class="language-${lang}">${markdown.utils.escapeHtml(
str
)}</code></pre>
</figure>`;
};
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");
// env.gallary.push(src)
}
return self.renderToken(tokens, idx, options);
};
markdown.renderer.rules.link_open = function (tokens, idx, options, env, self) {
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);
};
function 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);
});
}
markdown.core.ruler.push("toc", (state) => {
if (typeof state.env?.tocCallback === "function") {
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 headerName =
inline.children?.reduce((acc, token) => {
return acc + (token.type == "text" ? token.content : "");
}, "") || "";
let anchorName = encodeURIComponent(headerName || uuid());
token.attrPush(["id", anchorName]);
arr.push({
id: anchorName,
title: headerName,
level: parseInt(level),
children: [],
});
}
return arr;
}, []);
// 将tocArr 按level 变成树级
const tree = tocArr.reduce((acc: IToc[], item: IToc) => {
let currentLevel = acc;
let lastItem = null;
// Traverse the tree to find the correct parent based on the level
for (let i = 1; i < item.level; i++) {
if (lastItem && lastItem.children) {
currentLevel = lastItem.children;
} else {
// If no children array exists, create one
if (lastItem) {
lastItem.children = [];
currentLevel = lastItem.children;
}
}
lastItem = currentLevel[currentLevel.length - 1];
}
// Add the current item to the correct level
currentLevel.push(item);
return acc;
}, []);
state.env.tocCallback.call(undefined, tree);
}
});
export default defineNuxtPlugin(async (nuxtApp) => {
nuxtApp.provide("md", markdown);
});
Markdown.vue
html
<template>
<article
class="article prose prose-neutral dark:prose-invert max-w-full break-words"
>
<div v-html="content"></div>
</article>
<!-- 自写的简易图集展示 -->
<Gallery
:images="gallery.images"
:index="gallery.index"
v-model:show="gallery.show"
></Gallery>
</template>
<script lang="ts" setup>
import { lazyLoad } from "unlazy";
import type { IToc } from "~/types";
const props = defineProps<{
content: string;
toc?: boolean;
}>();
const emit = defineEmits<{
(e: "toc", toc: IToc[]): void;
}>();
const { $md } = useNuxtApp();
const colorMode = useColorMode();
const content = computed(() => {
return $md.render(props.content, {
toc: props.toc,
tocCallback(tocArr: IToc[]) {
emit("toc", tocArr);
},
});
});
//引入第三方的Prism.js代码
const thirdPartyCDN = [
"https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/prism/1.27.0/plugins/line-numbers/prism-line-numbers.min.css",
"https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/prism/1.27.0/components/prism-core.min.js",
"https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/prism/1.27.0/plugins/autoloader/prism-autoloader.min.js",
"https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/prism/1.27.0/plugins/line-numbers/prism-line-numbers.min.js",
"https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/mermaid/8.14.0/mermaid.min.js",
];
useHead(
thirdPartyCDN.reduce(
(acc, url, idx) => {
if (url.lastIndexOf(".css") > -1) {
acc.link.push({
hid: `thirdPartyCss${idx}`,
rel: "stylesheet",
href: url,
});
} else if (url.lastIndexOf(".js") > -1) {
acc.script.push({
hid: `thirdPartyJs${idx}`,
src: url,
defer: true,
tagPosition: "bodyClose",
});
}
return acc;
},
{ link: [], script: [] } as any
)
);
watch(
colorMode,
() => {
useHead({
link: [
{
rel: "stylesheet",
id: "prismTheme",
href: `https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/prism/1.27.0/themes/${
colorMode.value == "light" ? "prism" : "prism-tomorrow"
}.min.css`,
},
],
});
},
{
immediate: true,
}
);
const initMermaid = () => {
if (window.mermaid) {
try {
window.mermaid.initialize({ startOnLoad: true });
// 查找所有未渲染的mermaid图表并渲染
document
.querySelectorAll('.mermaid:not([data-processed="true"])')
.forEach((elem) => {
window.mermaid.init(undefined, elem);
});
} catch (error) {
console.error("Mermaid初始化失败:", error);
}
}
};
const gallery = reactive<{
images: string[];
index: number;
show: boolean;
}>({
images: [],
index: 0,
show: false,
});
watch(
() => props.content,
() => {
nextTick(() => {
lazyLoad("article img[loading='lazy']");
initPrism();
initMermaid();
});
},
{ immediate: false }
);
const { copy } = useClipboard();
onMounted(() => {
lazyLoad("article img[loading='lazy']");
initPrism();
initMermaid();
});
function initPrism() {
window.Prism?.highlightAll();
document.querySelectorAll("figure.code").forEach((item) => {
item.querySelector(".copy-icon")?.addEventListener("click", () => {
const text = item.querySelector("pre")?.textContent;
copy(text || "");
useToast("复制成功");
});
item.querySelector(".collapse-icon")?.addEventListener("click", () => {
item.classList.toggle("collapse");
});
});
document.querySelectorAll("article img").forEach((elem: Element, idx) => {
let src = elem.getAttribute("data-src") || elem.getAttribute("src");
if (src) {
gallery.images.push(src);
function handleClickImage() {
gallery.index = idx;
gallery.show = true;
}
elem.addEventListener("click", handleClickImage);
}
});
}
</script>
<style lang="stylus">
.article{
figure.code{
@apply: "relative rounded-md overflow-hidden b-1 auto-border-line";
pre{
@apply: "m-0 py-m1 bg-[#f9f9f9] dark:bg-[#272b33] rounded-none max-h-[70vh] transition-all duration-300";
}
.copy-icon{
@apply "cursor-pointer"
}
.header {
@apply: "flex justify-between items-center auto-text-title text-bm p-m2 b-b-1 auto-border-line";
.collapse-icon{
@apply "rotate-90 transition-transform duration-300 cursor-pointer"
}
div{
@apply: "flex items-center gap-m1";
}
}
.lang{
@apply: "auto-text-regular uppercase"
}
&.collapse{
.collapse-icon{
@apply "rotate-0"
}
pre{
@apply "max-h-0 py-0 overflow-hidden"
}
}
}
img{
@apply "max-h-70vh w-auto"
}
.column{
@apply "grid gap-m1 justify-items-center"
grid-template-columns: repeat(var(--article-columns), minmax(0, 1fr));
@apply "<md:grid-cols-1"
}
}
</style>