Obsidian 中的双括号链接是如何实现的?

前言

Obsidian 是一款非常流行的 markdown 本地笔记软件,其中有一个功能是其他笔记软件没有的,那就是双链笔记,通过链接在笔记之间建立关系,再通过可视化笔记关系,形成知识图谱,Obsidian 也因这个功能而知名。因此,我在想,是否能够让 MDX Editor 也拥有这个功能。

Obsidian 中各个笔记之间是通过双括号链接建立关系的,但双括号链接并不是标准的 markdown 语法,那在 markdown 中要如何实现这个功能呢?

Markdown 编译过程

Markdown 的解析过程需要用到 remarkrehype

  1. Markdown 文本被解析为 Markdown AST

  2. Markdown AST 可以由多个 remark plugins 操作

  3. Markdown AST 转换为 HTML AST

  4. 出于安全原因,HTML AST 需要被 sanitized

  5. HTML AST 可以由多个 rehype plugins 操作

  6. HTML AST 被字符串化为 HTML

  7. 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 下载桌面版体验。如果你对实现过程感兴趣,也可以直接查看源码

相关推荐
LateFrames8 分钟前
使用 Winform / WPF / WinUI3 / Electron 实现异型透明窗口
javascript·electron·wpf·winform·winui3
Asort12 分钟前
React类组件精要:定义机制与生命周期方法进阶教程
前端·javascript·react.js
陳陈陳14 分钟前
从“变量提升”到“调用栈爆炸”:V8 引擎是如何偷偷执行你的 JavaScript 的?
javascript
San3020 分钟前
深入理解JavaScript执行机制:从变量提升到内存管理
javascript·编程语言·代码规范
用户120391129472622 分钟前
深入理解JavaScript执行机制:从变量提升到调用栈全解析
javascript
weixin_4386943932 分钟前
pnpm 安装依赖后 仍然启动报的问题
开发语言·前端·javascript·经验分享
烟袅1 小时前
深入 V8 引擎:JavaScript 执行机制全解析(从编译到调用栈)
前端·javascript
有点笨的蛋1 小时前
JavaScript 执行机制深度解析:编译、执行上下文、变量提升、TDZ 与内存模型
前端·javascript
_一两风1 小时前
深入理解JavaScript执行机制:从一道经典面试题说起
javascript
阿凡达蘑菇灯1 小时前
langgraph---条件边
开发语言·前端·javascript