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

相关推荐
2501_946230984 小时前
Cordova&OpenHarmony通知中心实现
android·javascript
南山安5 小时前
JavaScript 函数柯里化:从入门到实战,一文搞定(面试可用)
javascript·面试·函数式编程
啃火龙果的兔子5 小时前
JavaScript 中的 Symbol 特性详解
开发语言·javascript·ecmascript
栀秋6665 小时前
你会先找行还是直接拍平?两种二分策略你Pick哪个?
前端·javascript·算法
漂流瓶jz5 小时前
PostCSS完全指南:功能/配置/插件/SourceMap/AST/插件开发/自定义语法
前端·javascript·css
南山安5 小时前
LangChain学习:Memory实战——让你的大模型记住你
前端·javascript·langchain
BD_Marathon6 小时前
Promise基础语法
开发语言·前端·javascript
Aotman_7 小时前
JavaScript MutationObserver用法( 监听DOM变化 )
开发语言·前端·javascript·vue.js·前端框架·es6
hashiqimiya7 小时前
JavaScript的object的使用和监控打印日志
前端·javascript·vue.js
梦6508 小时前
Vue 组件 vs React 组件深度对比
javascript·vue.js·react.js