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

相关推荐
柒和远方22 分钟前
V079: Milvus 向量数据库:AI 日记本的三组件部署、集合字段建模与余弦索引
javascript·sql
水獭比特24 分钟前
Anthropic Files / Skills 迁移:Workspace 不是租户隔离,API Key 也不是用户身份
javascript
默_笙35 分钟前
☕ 我给 TS 类型做了台"瘦身手术",还顺手用 Docker 起了个 MySQL
前端·javascript
ji_shuke2 小时前
Vue 3 使用 History 哨兵和 popstate 拦截浏览器返回
前端·javascript·vue.js·history·哨兵·popstate
名字还没想好☜2 小时前
Prometheus 告警实战:写 alerting rules、用 Alertmanager 做路由分组与抑制
运维·前端·javascript·docker·kubernetes·prometheus
mONESY3 小时前
手把手从零复刻「英文网页 AI 翻译」Chrome 插件(新手向)
javascript
iFlyCai4 小时前
深入理解Flutter:StatefulWidget生命周期全解析(四)
前端·javascript·flutter
夜雨声烦丿4 小时前
从需求到页面:日期计算器应用的 ArkTS 原生实现
开发语言·javascript·华为·harmonyos
宇智波亚索5 小时前
TypeScript 实际应用
前端·javascript·typescript
:-)6 小时前
idea中的vue文件没有高亮显示
前端·javascript·vue.js·ecmascript·intellij-idea