前言
Obsidian 是一款非常流行的 markdown 本地笔记软件,其中有一个功能是其他笔记软件没有的,那就是双链笔记,通过链接在笔记之间建立关系,再通过可视化笔记关系,形成知识图谱,Obsidian 也因这个功能而知名。因此,我在想,是否能够让 MDX Editor 也拥有这个功能。
Obsidian 中各个笔记之间是通过双括号链接建立关系的,但双括号链接并不是标准的 markdown 语法,那在 markdown 中要如何实现这个功能呢?
Markdown 编译过程
Markdown 的解析过程需要用到 remark 和 rehype
-
Markdown 文本被解析为 Markdown AST
-
Markdown AST 可以由多个 remark plugins 操作
-
Markdown AST 转换为 HTML AST
-
出于安全原因,HTML AST 需要被 sanitized
-
HTML AST 可以由多个 rehype plugins 操作
-
HTML AST 被字符串化为 HTML
-
HTML 渲染后的一些特除操作处理
整个编译过程如以下流程图:
实现
通过 astexplorer 这个网站,可以直接查看 Markdown AST。
比如有以下一个最简单的 Markdown 文本。
注意在顶部选择解析器为 Markdown 和 remark。
在右侧可以直接查看 Markdown AST。
在其底部有个最简单的 remark 插件示例
js
// available utilities are: "unist-util-is", "unist-util-visit", and "unist-util-visit-parents"
const visit = require("unist-util-visit");
module.exports = function attacher(options) {
return function transformer(tree, vfile) {
// add a level to headings, for example `# heading` to `## heading`
visit(tree, "heading", (node) => {
node.depth += 1
});
return tree;
};
};
这个插件可以将 标题一(H1) 转换成标题二(H2)
双括号文本转换
一个双括号链接的文本转换成 AST
如下图所示
因此我们可以通过一个 remark Plugin 来实现转换
也就是需要转换成如下 md
转换成如下图这样一个 AST 结构
通过一个正则表达式来匹配 AST 中的 Text 节点
js
const parenthesisRegexExclusive = /(?<=\[\[).*?(?=\]\])/g
const matches = value.match(parenthesisRegexExclusive)
我们可以实现一个 remark 插件,将双括号中的文本转为链接
js
const visit = require("unist-util-visit");
module.exports = function attacher(options) {
return function transformer(tree, vfile) {
visit(tree, 'text', (node, index, parent) => {
const value = node.value
if (
typeof value !== 'string' ||
!parent ||
!Array.isArray(parent.children) ||
parent.type === 'link' ||
parent.type === 'linkReference'
) {
return
}
const parenthesisRegexExclusive = /(?<=\[\[).*?(?=\]\])/g
const matches = value.match(parenthesisRegexExclusive)
if (!matches) {
return
}
const children = [value]
matches.forEach((match) => {
const last = children.pop()
if (typeof last !== 'string') {
return
}
const split = `[[${match}]]`
const [first, ...rest] = last.split(split)
children.push(first, { id: match }, rest.join(split))
})
parent.children.splice(
index,
1,
...children.map((child) => {
if (typeof child === 'string') {
return {
type: 'text',
value: child,
}
}
return {
type: 'link',
url: child.id,
children: [{ type: 'text', value: child.id }],
}
})
)
})
return tree;
};
};
实现地址
astexplorer.net/#/gist/5b6a...
最后可以在渲染的 html 的中添加脚本,阻止非 HTTP 开头的链接。
js
document.body.addEventListener("click", (event) => {
event.preventDefault();
let el = event.target;
if (!el || el.nodeName !== "A" || !el.getAttribute("href")) return;
const href = el.getAttribute("href");
// http 开头是远程地址
if (/^https?:\/\//.test(href)) {
openLink(href);
} else {
// 通过文件名找当前的目录地址
}
});
这样就实现了双括号链接。
最后
MDX Editor 桌面 App 已实现了双括号链接,可以在 Github 下载桌面版体验。如果你对实现过程感兴趣,也可以直接查看源码。