【QuickAdd脚本】文档转换与备份管理

首先说明,这是一篇根据我的个人需求编写的小脚本分享。我的 Obsidian 工作流程尚未完全成熟,而且经常变动,因此这脚本可能对他人用处不大。本文单纯是分享个人经验,尽管它们可能在某些人看来并无实际用途。

emmm… 我觉得自己在论坛上发了很多没用的脚本,对此给论坛带来污染我深感抱歉:joy:,非常感谢论坛的宽容和大家的观看。

个人需求说明

首先,介绍一下我的需求。我喜欢使用 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 结构来管理,如下所示:

  • :file_folder:Obsidian
    • :page_facing_up:Obsidian.md
    • :art:Obsidian.excalidraw.md
    • :diamond_shape_with_a_dot_inside:Obsidian.canvas
    • …(其他子笔记)

显然,每次需要创建 .excalidraw.canvas 这些文档时,我都得先创建文件夹,然后再创建同名的画板或白板文件,这个过程有点繁琐。之前我一直将就着用,因为这种情况并不多见。然而,目前基于此工作流程,我编写了这个脚本,还新增了备份功能,以适应我可能需要的多种版本需求。

实现功能

  • 自动构建 FolderNote 笔记结构
  • 返回或创建主笔记
  • 创建同名 Excalidraw 画板
  • 创建同名 Canvas 白板
  • 备份不同类型笔记

操作演示

24.06.20_QuickAdd脚本-文档转换与备份管理_IMG-2

推荐配合插件

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 };
}

参考资料


如有任何具体内容需要进一步修改,欢迎随时告知!

3 个赞