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

相关推荐
罗_三金15 分钟前
前端框架对比和选择?
javascript·前端框架·vue·react·angular
Fan_web1 小时前
JavaScript高级——闭包应用-自定义js模块
开发语言·前端·javascript·css·html
叫我:松哥1 小时前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap
好名字08212 小时前
monorepo基础搭建教程(从0到1 pnpm+monorepo+vue)
前端·javascript
c#上位机2 小时前
C#事件的用法
java·javascript·c#
万物得其道者成2 小时前
React Zustand状态管理库的使用
开发语言·javascript·ecmascript
小白小白从不日白2 小时前
react hooks--useReducer
前端·javascript·react.js
下雪天的夏风3 小时前
TS - tsconfig.json 和 tsconfig.node.json 的关系,如何在TS 中使用 JS 不报错
前端·javascript·typescript
diygwcom3 小时前
electron-updater实现electron全量版本更新
前端·javascript·electron
volodyan3 小时前
electron react离线使用monaco-editor
javascript·react.js·electron