我是如何像Obsidian Publish那样在本地集成Graph View的

Obsidian Publish 在笔记中集成了 Graph View,让浏览笔记的时候能够查看与当前笔记关联的知识图谱,帮助进行结构化地理解和思考。

比较遗憾的是Obsidian App原生没有支持该能力——将graph view集成到笔记中进行预览,这篇文章就是介绍一下我是如何实现这个能力的,先看一下效果。

0:00
/0:10

设计思路

local graph是由Obsidian内部的引擎计算实现的,由canvas绘制图形(类似chartjs),我只需要将canvas元素插入到当前笔记的workspace leaf中即可:

  1. 通过local graph命令绘制canvas图形;
  2. 将canvas图形插入到当前笔记合适的位置;
  3. 去除canvas原先的view(window);

一步一坑

1. local graph绘制

直接使用obsidian原生的local graph命令绘制当前笔记的关联图谱的view。

await this.app.commands.executeCommandById("graph:open-local");

在实验过程中发现,如果这样的话执行的话会创建split view,直接影响obsidian的使用体验。

经过权衡,可以通过创建独立的window来解决这个问题:

await this.app.commands.executeCommandById("graph:open-local");
await new Promise((resolve) => setTimeout(resolve, 200));
const graphLeaf = this.app.workspace.getLeavesOfType('localgraph')[0];
this.app.workspace.moveLeafToPopout(graphLeaf);

2. canvas插入当前笔记

这一步骤属于基础的html query操作,通过view-content找到local graph的元素,然后将local graph的canvas插入到指定元素的位置(当前选择的是view-header元素)。

const localgraph = graphLeaf.view.containerEl.getElementsByClassName('view-content')[0];
const noteHeader = fileLeaf.containerEl.getElementsByClassName('view-header')[0];
noteHeader.parentElement.insertAfter(localgraph, noteHeader);

在实验过程中发现,发现了几个问题:

  • element占用空间太大了;
  • tab 切换的时候会出现空白的html element残留;

通过unlink tab以及style自定义解决这些问题:

const localgraph = graphLeaf.view.containerEl.getElementsByClassName('view-content')[0];
localgraph.style.width = '80%';
localgraph.style.height = '360px';
localgraph.style.alignSelf = 'center';
localgraph.addClass('embed-local-graph-with-personal-assistant');
// unlink
graphLeaf.tabHeaderStatusLinkEl.click();

const noteHeader = fileLeaf.containerEl.getElementsByClassName('view-header')[0];
noteHeader.parentElement.insertAfter(localgraph, noteHeader);

3. tab切换问题

obsidian tab切换的时候会发现几个问题:

  • 重复插入canvas;
  • graph view配置menu默认打开;
  • graph view没有继承颜色等配置;
await this.app.commands.executeCommandById("personal-assistant:set-local-graph-view-colors");
fileLeaf.view.containerEl.getElementsByClassName('graph-controls-button')[0].click();

this.app.workspace.on('file-open', (file) => {
    // console.log(file);
    // let fileName = file ? file.basename : "--";
    // console.log(fileName);
    // if (this.app.workspace.activeLeaf.getDisplayText() !== fileName) {
    //     console.log("back"); return;
    // }

    const wins = BrowserWindow.getAllWindows();
    const graphWindow2Close = wins.find((win) => {
        //return win.getTitle().startsWith("Graph") && win.id === mainWinID;
        return !win.isVisible()
    },);
    if (graphWindow2Close) {
        console.log(graphWindow2Close.getTitle());
        graphWindow2Close.close();
    } else {
        fileLeaf.view.containerEl.getElementsByClassName('embed-local-graph-with-personal-assistant')[0]?.remove();
    }
},);

4. 功能触发

通过dataview可以借助在任意笔记文件中执行js的能力实现该功能的触发,这样可以保证在打开笔记文件的时候就会触发local graph集成的。

后续

当前这样的实现的思路勉强可以满足需求,但是还有几个缺陷不足:

  • dataviewjs 触发执行的时机是lazy的,这会导致打开文件之后插入canvas的动作有滞后;
  • 由于electron window接口的问题,会导致editor/preview视图切换的时候出现显示问题;
  • 偶尔导致obsidian app闪退;

对于这样的问题看是需要通过插件来实现,后续我打算把这个功能集成personal assistant中做到自动化开关和配置。

附录

// 注意在 obsidian 中通过 `dataviewjs` 来触发,也可以配置到模版中
const { BrowserWindow } = require("@electron/remote");

const fileLeaf = this.app.workspace.activeLeaf;
if (fileLeaf.view.containerEl.getElementsByClassName('embed-local-graph-with-personal-assistant').length > 0) {
    console.log("already embedded");
    return;
}
const mainWin = BrowserWindow.getAllWindows();
if (mainWin.length !== 1) {
    // new file tab to embed local graph
    for (let i = 0; i < mainWin.length; i++) {
        if (mainWin[i].getTitle().startsWith("Graph of")) {
            new Notice("closing embedded local graph windown");
            mainWin[i].close();
        }
    }
    return;
}
const mainWinID = mainWin[0].id;

await this.app.commands.executeCommandById("graph:open-local");
//await this.app.commands.executeCommandById("personal-assistant:local-graph");
await new Promise((resolve) => setTimeout(resolve, 100));

const graphLeaf = this.app.workspace.getLeavesOfType('localgraph')[0];
this.app.workspace.moveLeafToPopout(graphLeaf);
const winsFocus = BrowserWindow.getFocusedWindow();
winsFocus.hide();

// unlink
graphLeaf.tabHeaderStatusLinkEl.click();

const localgraph = graphLeaf.view.containerEl.getElementsByClassName('view-content')[0];
localgraph.style.width = '80%';
localgraph.style.height = '360px';
localgraph.style.alignSelf = 'center';
localgraph.addClass('embed-local-graph-with-personal-assistant');

const noteHeader = fileLeaf.containerEl.getElementsByClassName('view-header')[0];
noteHeader.parentElement.insertAfter(localgraph, noteHeader);

await this.app.commands.executeCommandById("personal-assistant:set-local-graph-view-colors");
fileLeaf.view.containerEl.getElementsByClassName('graph-controls-button')[0].click();

this.app.workspace.on('file-open', (file) => {
    // console.log(file);
    // let fileName = file ? file.basename : "--";
    // console.log(fileName);
    // if (this.app.workspace.activeLeaf.getDisplayText() !== fileName) {
    //     console.log("back"); return;
    // }

    const wins = BrowserWindow.getAllWindows();
    const graphWindow2Close = wins.find((win) => {
        //return win.getTitle().startsWith("Graph") && win.id === mainWinID;
        return !win.isVisible()
    },);
    if (graphWindow2Close) {
        console.log(graphWindow2Close.getTitle());
        graphWindow2Close.close();
    } else {
        fileLeaf.view.containerEl.getElementsByClassName('embed-local-graph-with-personal-assistant')[0]?.remove();
    }
},);

References

  1. Notion Backlinks
  2. Obsidian Publish