Obsidian-Digital-Garden 文件树实现原理
在 Obsidian-Digital-Garden 中,支持左侧以文件树形式展示一个目录。
左侧目录在数字花园中是非常重要的。我曾尝试不断优化文章的内容,希望以文章替代目录。但文章读起来还是太“累”了。
Obsidian-Digital-Garden 的文件树是根据物理路径生成的。只要我们在 0.0 Obsidian 介绍 中,合理规划路径,在数字花园中即可直观展示。这对于书籍形式的写作是非常方便的。
但是,回到我个人的场景中,我却不太喜欢这种物理目录的文件树。以写书场景为例,文章与目录(路径)耦合后,调整目录就会将文章由一个文件夹移动到另一个,这对导致文章的 URL 变化。我喜欢采用 Build in Public 的形式,尽早发布。这会导致我今天发出去的文章,明天 URL 变了大家都找不到,搜索引擎也找不到。
因此,我更加喜欢的是,将左侧边栏目录与文件系统解耦,我通过一个 JavaScript 结构声明目录。Obsidian-Digital-Garden 并不支持这一特性。
在本文中,有两个目标,一是梳理 Obsidian-Digital-Garden 文件树的实现原理,二是探索如何 Hack 出我想要的目录形式。
如何开启
对于普通用户来说,可在 0.0 Obsidian 介绍 的 Digital Garden 的 Global Note Settings 下进行开启,如下图所示:
这个开关对应于 Obsidian-Digital-Garden 模版工程根目录的 .env
中的 dgShowFileTree:
dgShowFileTree=false
效果截图
开启后,下次发布笔记时,数字花园的左侧将会出现文件树。
值得称赞的是,文件树的界面采用响应式设计,宽屏时常驻,窄屏时通过菜单按钮开关,并且对移动端适配良好。效果如下:
注:图中的目录并非来自于文件系统,而是我修改了模板代码,手动编辑而成。具体做法可见后面小节。
文件树协议
为了支持自定义文件树,脱离文件系统,我分析了文件树的协议如下:
{
'000.wiki': {
isFolder: true,
'HomePage.md': {
isNote: true,
permalink: '/',
name: 'HomePage',
noteIcon: '',
hide: false,
pinned: false
}
}
}
从中可以看出,还是非常简单的。这个结果支持嵌套。
采用自定义文件树
这一步需要对模板工程的源码进行修改。修改方法并不唯一,这里给我我的改法:
首先,在 src/helpers/userUtils.js
中添加目录:
category = {
'Obsidian': {
isFolder: true,
'使用基础': {
isFolder: true,
'.obsidian 目录': {
isNote: true,
permalink: '/000.wiki/Obsidian .obsidian 目录/',
name: '.obsidian 目录',
},
'我的快捷键': {
isNote: true,
permalink: '/000.wiki/我的 Obsidian 快捷键/',
name: '我的 Obsidian 快捷键',
},
},
//...
},
},
exports.category = category;
之后,修改 src/site/_data/eleventyComputed.js
:
const { getGraph } = require("../../helpers/linkUtils");
const { getFileTree } = require("../../helpers/filetreeUtils");
const { userComputed, category } = require("../../helpers/userUtils");
module.exports = {
graph: (data) => getGraph(data),
filetree: (data) => category,
userComputed: (data) => userComputed(data)
};
其中,跳过了 getFileTree 方法,直接给出我们手动编辑的 category。
在 Obsidian 中维护文件树
使用 JSON 来维护数字花园侧边栏目录的方案,运行了一段时间之后,我发现有两个问题:一是 JSON 手动编辑起来太麻烦,二是文件树是脱离 Obsidian 的,造成了工作流的割裂。
于是,我想到,在 Obsidian 中创建一个 Markdown 页面,用嵌套列表来维护这个文件树。这个 Markdown 页面可以直接拖动到 Obsidian 侧边栏,当作目录使用。
给定如下 Markdown:
- 关于我
- 归档
- 个人成长
- [[Maeiee的成长周报]]
- [[读书记录]]
- Maeiee思考
- [[Maeiee思考1:我在AI浪潮中的位置]]
- [[Maeiee思考2:LLM连续微调游戏]]
- Obsidian
- Obsidian 介绍
目标:编写 Node.js 脚本,将上述 Markdown 格式转换为如下 JSON:
category = {
'关于我': {
isNote: true,
permalink: '/000.wiki/Maeiee的自我介绍/',
name: '关于我',
},
'归档': {
isNote: true,
permalink: '/000.wiki/数字花园归档页/',
name: '归档',
},
'个人成长': {
isFolder: true,
'Maeiee的成长周报': {
isNote: true,
permalink: '/000.wiki/Maeiee的成长周报/',
name: 'Maeiee的成长周报',
},
'读书记录': {
isNote: true,
permalink: '/006.电子书/读书记录/',
name: '读书记录',
},
'Maeiee思考': {
isFolder: true,
'Maeiee思考1:我在AI浪潮中的位置': {
isNote: true,
permalink: '/000.wiki/Maeiee思考1:我在AI浪潮中的位置/',
name: 'Maeiee思考1:我在AI浪潮中的位置',
},
'Maeiee思考2:LLM连续微调游戏': {
isNote: true,
permalink: '/000.wiki/Maeiee思考2:LLM连续微调游戏/',
name: 'Maeiee思考2:LLM连续微调游戏',
},
},
},
'Obsidian': {
isFolder: true,
'Obsidian 介绍': {
isNote: true,
permalink: '/000.wiki/Obsidian/',
name: 'Obsidian 介绍',
},
}
对比 Markdown 与 JSON,有如下细节需要注意:
- 支持任意级别嵌套
- Markdown 中包含笔记名称以及别名
- 笔记名称对应于 permalink
- 别名对应于 name,以及作为 isNote 的结构的 Key
- Markdown 中的非链接格式的本文对应于 JSON 中的目录(isFolder)
- Markdown 中未包含笔记的完整路径,这是一个缺失内容,需要通过下面查询方法
有一个变量 const cat = data.collections.note;
,是一个列表,需要通过遍历用笔记名称找出对应的地址:
let title = cat[n].data.title || cat[n].fileSlug;
let url = cat[n].url;
数字花园站点生成器是一个基于 eleventy 的静态站点,我该在哪一步加入上述脚本的?我理解是在有了所有笔记数据后,但是在具体执行静态生成器前,因为我这里生成的 JSON,需要用于页面的生成。
在 src/site/_data
下有一个 eleventyComputed.js:
const { getGraph } = require("../../helpers/linkUtils");
const { getFileTree } = require("../../helpers/filetreeUtils");
const { userComputed, category } = require("../../helpers/userUtils");
module.exports = {
graph: (data) => getGraph(data),
filetree: (data) => category,
userComputed: (data) => userComputed(data)
};
其中 category 是我修改之前人为编辑 JSON 的实例,data 是站点全量数据。我想我找到地方了。
在 GPT 的帮助下,最终实现代码如下:
const { getGraph } = require("../../helpers/linkUtils");
const { getFileTree } = require("../../helpers/filetreeUtils");
const { userComputed, category } = require("../../helpers/userUtils");
const fs = require('fs'); // 导入fs模块
let cachedCategory = null;
module.exports = {
graph: (data) => getGraph(data),
filetree: (data) => {
if (cachedCategory) {
return cachedCategory;
}
const cat = data.collections.note || [];
if (cat.length > 0) {
const parseMarkdownToJSON = (md) => {
const lines = md.split('\n')
.filter(line => !line.startsWith('---')) // 移除元数据
.filter(line => !line.includes('dg-publish')) // 移除未发布的笔记
.filter(line => line.trim() !== ''); // 移除空行
const stack = [{ level: -1, obj: {} }];
lines.forEach(line => {
const level = line.lastIndexOf('\t') + 1; // 使用缩进级别来确定当前行的层级
const text = line.trim().replace(/^[\s\-]+/g, '').replace(/[\[\]]/g, ''); // 移除Markdown语法字符
// 如果是 Obsidian 内链,表示是笔记
const isNote = (line.indexOf("[[") !== -1 && line.indexOf("]]") !== -1);
let name, alias;
if (isNote) {
[permalink, name] = text.split('\|');
name = name.trimLeft();
permalink = '/' + permalink.replace('\\', '/').trimLeft();
} else {
name = alias = text.trimLeft();
}
// 创建新的层级/笔记对象
const newItem = isNote ? {
isNote: true,
permalink: `${permalink}`,
name: name,
} : {
isFolder: true,
};
// 处理层级逻辑
while (stack[stack.length - 1].level >= level) {
stack.pop();
}
stack[stack.length - 1].obj[name] = newItem; // 将新项目添加到当前层级
stack.push({ level, obj: newItem }); // 将新项目压入堆栈
});
return stack[0].obj; // 返回最顶层对象
};
const mdContent = fs.readFileSync('src/site/notes/000.wiki/数字花园目录.md', 'utf8');
const fileTree = parseMarkdownToJSON(mdContent);
cachedCategory = fileTree;
console.log(cachedCategory);
return fileTree;
}
},
userComputed: (data) => userComputed(data)
};
本文作者:Maeiee
本文链接:Obsidian-Digital-Garden 文件树实现原理
版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!
喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!