雪的日记

构建一个 mdx-remark 插件

· #Code

什么是 MDX?

MDX 是MarkDown的超集, 在支持MarkDown语法的基础上, 允许我们引入jsx, 以使用更丰富的组件

同时 MDX 提供了 remark 和 rehype 的插件机制, 使得可以在 MDX 中处理MarkDown标签, 使我们可以更加自由的渲染页面

什么是 remark 和 rehype?

remark 用于处理mdAST, rehype 用于处理htmlAST

要想理解他们, 首先我们要了解AST(Abstract Syntax Tree): 抽象语法树

它通过树状结构描述了代码的语法结构, 使我们可以对代码进行分析和处理, 可以参考如下文件:

MarkDown
# 标题
```js
const a = 1
```
MarkDown-AST
{
"type": "root",
"children": [
{
"type": "heading",
"depth": 1,
"children": [
{
"type": "text",
"value": "标题"
}
]
},
{
"type": "code",
"lang": "js",
"value": "const a = 1"
}
]
}
简化的HTML
<h1>标题</h1>
<pre><code class="language-js">const a = 1</code></pre>

开始编写一个 remark 插件

安装好必要的依赖后, 首先在配置中引入插件 (以 astro 为例)

config.mjs
import { defineConfig } from "astro/config";
import { unified } from "@astrojs/markdown-remark";
import { pluginFunc } from "./remark-plugin.mjs";
export default defineConfig({
markdown: {
processor: unified({
remarkPlugins: [pluginFunc],
rehypePlugins: [],
}),
}
})

其中remark-plugin.mjs是自定义的插件

pluginFunc是一个返回(tree)=>{...}的闭包函数

其最简示例如下:

remark-plugin.mjs
export function remarkContainer() {
return (tree) => {
const { children } = tree;
let index = 0;
while (index < children.length) {
const node = children[index];
// 寻找起始标记 :::name
if (
node.type === "paragraph" &&
node.children.length === 1 &&
node.children[0].type === "text"
) {
const marker = node.children[0].value.match(/^:::(\w+)\s*$/);
if (marker) {
const name = marker[1];
let closeIndex = -1;
// 寻找闭合标记
for (let i = index + 1; i < children.length; i++) {
const c = children[i];
if (
c.type === "paragraph" &&
c.children.length === 1 &&
c.children[0].type === "text" &&
c.children[0].value.trim() === ":::"
) {
closeIndex = i;
break;
}
}
// 修改内容
if (closeIndex !== -1) {
const containerChildren = children.slice(index + 1, closeIndex);
const newNode = {
type: "newContainer",
name: name,
children: containerChildren,
};
children.splice(index, closeIndex - index + 1, newNode);
index++;
continue;
}
}
}
index++;
}
};
}

重点分为两个步骤, 1.寻找标记, 2.修改内容

以上函数会将:::name ... :::打包到新的节点

代码演示

MarkDown
# 标题
:::warning
这是**警告**
:::
AST-原始
{
"type": "root",
"children": [
{
"type": "heading",
"depth": 1,
"children": [ { "type": "text", "value": "标题" } ]
},
{
"type": "paragraph",
"children": [ { "type": "text", "value": ":::warning" } ]
},
{
"type": "paragraph",
"children": [
{ "type": "text", "value": "一个" },
{ "type": "strong", "children": [ { "type": "text", "value": "警告" } ] }
]
},
{
"type": "paragraph",
"children": [ { "type": "text", "value": ":::" } ]
}
]
}
AST-处理后
{
"type": "root",
"children": [
{
"type": "heading",
"depth": 1,
"children": [ { "type": "text", "value": "标题" } ]
},
{
"type": "newContainer",
"name": "warning",
"children": [
{
"type": "paragraph",
"children": [
{ "type": "text", "value": "一个" },
{ "type": "strong", "children": [ { "type": "text", "value": "警告" } ] }
]
}
]
}
]
}
简化的HTML
<h1>标题</h1>
<div class="warning">
<p>这是一个<strong>警告</strong></p>
</div>