首先说明,这是一篇根据我的个人需求编写的小脚本分享。我的 Obsidian 工作流程尚未完全成熟,而且经常变动,因此这脚本可能对他人用处不大。本文单纯是分享个人经验,尽管它们可能在某些人看来并无实际用途。
emmm… 我觉得自己在论坛上发了很多没用的脚本,对此给论坛带来污染我深感抱歉,非常感谢论坛的宽容和大家的观看。
个人需求说明
首先,介绍一下我的需求。我喜欢使用 Excalidraw 画板来临时存放各种素材,作为草稿图。如果有一篇笔记中的某些图片需要重构,我会在 Excalidraw 上重新绘制。为了避免画板数量过多,通常一个画板会存放一个系列笔记的草稿图,整理完之后,我会将它们复制为图片导入到对应的笔记中。我并没有使用 Excalidraw 的块引用,因为我担心它的不稳定和加载速度较慢,所以只用 Markdown 笔记记录文字和图片。尽管如此,这些承载着素材的 Excalidraw 文件我并不舍得删除,也许很久之后我会修改它们,也有可能永远都不会再看了。
为了让 Markdown 和 Excalidraw 紧密联系,我会让它与主笔记(Index)同名,后缀加 .excalidraw
。比如:Obsidian笔记.md
对应 Obsidian.excalidraw.md
。
同理,对于纯线性的 Markdown 笔记,有时候不利于整理思路,Obsidian 的 Canvas 白板是个极好的工具。于是,同系列的笔记我也可能会用 Canvas 来整理,对应的 Canvas 文件会与主笔记同名,后缀为 .canvas
。比如:Obsidian笔记.md
对应 Obsidian.canvas
。
我用 FolderNote 结构来管理,如下所示:
- Obsidian
- Obsidian.md
- Obsidian.excalidraw.md
- Obsidian.canvas
- …(其他子笔记)
显然,每次需要创建 .excalidraw
或 .canvas
这些文档时,我都得先创建文件夹,然后再创建同名的画板或白板文件,这个过程有点繁琐。之前我一直将就着用,因为这种情况并不多见。然而,目前基于此工作流程,我编写了这个脚本,还新增了备份功能,以适应我可能需要的多种版本需求。
实现功能
- 自动构建 FolderNote 笔记结构
- 返回或创建主笔记
- 创建同名 Excalidraw 画板
- 创建同名 Canvas 白板
- 第一次创建会自动平铺主笔记内容,详见 PKMer_QuickAdd 脚本 - 利用 Canvas 平铺笔记,不过具体平铺参数只能手动修改源码调整。
- 备份不同类型笔记
操作演示
推荐配合插件
- Folder notes (v1.7.22,作者:Lost Paul)
- Quick Explorer (v0.2.8,作者:PJEby)
- 最好取消自动预览的功能,详见:PKMer_如何关闭 Quicker Explorer 插件的自动预览
Quickadd Macro 脚本
配置流程:
const path = require('path');
// 获取Excalidraw默认模版路径
const excalidrawTemplatePath = (app.plugins.plugins["obsidian-excalidraw-plugin"].settings["templateFilePath"]).replace(/\.md/, "") + ".md";
module.exports = async (params) => {
const quickAddApi = app.plugins.plugins.quickadd.api;
const file = app.workspace.getActiveFile();
const regex = /\son\s\d{2}\-\d{2}\-\d{2}_\d{2}\.\d{2}(\.\d{2})?$/;
const timeFormat = "[ on ]YY-MM-DD_HH.mm";
const fileName = file.basename.replace(regex, "").replace(/\.excalidraw$/, "");
let fileExt = file.extension;
const fileDir = path.dirname(file.path);
let newFileName = "";
const options = [`📝笔记:${fileName}.md`, `🎨画板:${fileName}.excalidraw`, `💠白板:${fileName}.canvas`, `📅备份:${file.basename.replace(regex, "") + window.moment().format(timeFormat)}.${fileExt}`];
const select = await quickAddApi.suggester(options, options);
if (!select) return;
if (fileName !== path.basename(fileDir)) {
const folderPath = fileDir + "/" + fileName;
const isCreat = await quickAddApi.yesNoPrompt("是否创建FolderNote?", `未检测到📁【${folderPath}】文件夹,该脚本需要在Foldernote的结构下创建文档副本,是否自动创建?`);
if (!isCreat) return;
console.log(folderPath);
// 直接的./不识别,最好加上.replace(/^\.\//,"")
const isFolderNote = await app.vault.getFolderByPath(folderPath.replace(/^\.\//, ""));
console.log(isFolderNote);
if (!isFolderNote) {
await app.vault.createFolder(folderPath);
}
const destinationPath = path.join(folderPath, file.basename.replace(regex, "") + "." + fileExt);
await app.fileManager.renameFile(app.vault.getAbstractFileByPath(file.path), destinationPath);
new Notice(`已构建FolderNote结构!`);
return;
}
let content = "";
// 🎨Excalidraw
if (select === options[1]) {
fileExt = "md";
const file = await app.vault.getAbstractFileByPath(excalidrawTemplatePath);
content = await app.vault.read(file);
newFileName = `${fileName}.excalidraw`;
}
// 💠Canvas
else if (select === options[2]) {
fileExt = "canvas";
newFileName = `${fileName}`;
content = await convertMdToCanvas(file);
}
// 📄主笔记
else if (select === options[0]) {
fileExt = "md"; content = "";
newFileName = `${fileName}`;
}
// 📅备份
else {
fileExt = "md";
// const file = await app.vault.getAbstractFileByPath(excalidrawTemplatePath);
content = await app.vault.read(file);
if (/\.excalidraw$/.test(file.basename.replace(regex, ""))) {
newFileName = fileName + ".excalidraw" + window.moment().format(`${timeFormat}`);
} else if (file.extension === "canvas") {
fileExt = "canvas";
newFileName = fileName + window.moment().format(`${timeFormat}`);
} else {
newFileName = fileName + window.moment().format(`${timeFormat}`);
}
const isCreat = await quickAddApi.yesNoPrompt("是否备份副本?", `是否备份为【${newFileName}.${fileExt}】,这样的文件往往是冗余的......`);
if (!isCreat) return;
}
const newFilePath = `${fileDir}/${newFileName}.${fileExt}`;
console.log(newFilePath);
let newFile = app.vault.getAbstractFileByPath(newFilePath);
if (!newFile) {
newFile = await app.vault.create(newFilePath, content);
if (select === options[3]) return;
await app.workspace.activeLeaf.openFile(newFile);
await app.workspace.activeLeaf.rebuildView();
return;
}
await app.workspace.activeLeaf.openFile(newFile);
};
// 第一次转换Canvas会平铺主笔记
async function convertMdToCanvas(file) {
// 可调节的参数
// 大纲等级
const level = 2;
// 卡片参数
const width = 660;
const height = 800;
// 卡片间隔
const space = 50;
// 每行卡片的数量限制
const limit = 4;
const canvasData = {
nodes: [],
edges: []
};
console.log("开始获取二级标题");
const { heads, counts } = await getHeadings(file, level);
console.log(heads);
let x = 0; let y = 0; let n = 1;
let nodes = [];
const length = heads.length;
for (let i = 1; i <= length; i++) {
const node = {
id: "", type: "file", file: file.path, subpath: "", x: 0, y: 0, width: width, height: height,
};
node.subpath = heads[i - 1];
node.id = String(i);
node.x = x; node.y = y;
// node.color = String(counts[i - 1] - 1);
console.log([heads[i - 1], x, y]);
x += width + space;
if (i >= limit * n) {
y += height + space;
x = 0;
n = n + 1;
}
console.log([heads[i - 1], node.x, y]);
nodes.push(node);
}
canvasData.nodes = nodes;
// console.log(canvasData);
const canvasJson = JSON.stringify(canvasData, null, 2);
return canvasJson;
}
async function getHeadings(file, level) {
// 读取文件内容
const fileContent = await app.vault.read(file);
// 使用正则表达式提取指定级别的标题
const regex = new RegExp(`^#{1,${level}}\\s(.+)`, 'gm');
const heads = [];
let head;
let counts = [];
while ((head = regex.exec(fileContent)) !== null) {
heads.push("#" + head[1]);
counts.push(head[0].match(/#/g).length);
}
return { heads, counts };
}
参考资料
- PKMer_QuickAdd 脚本 - 利用 Canvas 平铺笔记
- PKMer_QuickAdd 脚本 - F2 弹窗式重命名三合一
- PKMer_QuickAdd 脚本 - 移动子笔记或附件到当前文件夹
如有任何具体内容需要进一步修改,欢迎随时告知!