Skip to content

Commit

Permalink
Merge pull request #575 from PandaNocturne/addExJsBookxnote
Browse files Browse the repository at this point in the history
 自定义 Excalidraw 脚本 - Excalidraw 与 BookxNote 的联动
  • Loading branch information
windily-cloud authored Dec 20, 2023
2 parents f61d185 + b7dce99 commit e843e1a
Show file tree
Hide file tree
Showing 3 changed files with 365 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,13 @@ Excalidraw 在早期是有很多问题的,比如没有手写压感,插图太
- [[Excalidraw脚本-Slideshow完美实现画板幻灯片演示的脚本]]
- [[Excalidraw局部嵌入Obsidian]]
- [[自定义Excalidraw脚本-实现Zotero与Excalidraw的拖拽联动]]
- [[自定义Excalidraw脚本-实现Excalidraw与BookxNote的联动]]
- [[自定义Excalidraw脚本-建立库外Eagle素材库的连接]]
- [[自定义Excalidraw脚本-画板局部或者全局播放动画]]
- [[自定义Excalidraw脚本-快速插入时间戳笔记]]
- [[自定义Excalidraw脚本-OCR自动提取图片文字]]
- [[自定义Excalidraw脚本-默认应用打开图片]]
- [[Excalidraw自定义脚本-插入可以编辑的Mermaid图形]]
- [[自定义Excalidraw脚本-插入可以编辑的Mermaid图形]]

### CSS 美化

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
---
uid: 20231220152351
title: 自定义 Excalidraw 脚本 - 实现 Excalidraw 与 BookxNote 的联动
tags: [工作流, pdf标注, Excalidraw脚本, BookxNote]
description: " 自定义 Excalidraw 脚本 - Excalidraw 与 BookxNote 的联动"
author: 熊猫别熬夜
type: other
draft: false
editable: false
modified: 20231220155036
---

# 自定义 Excalidraw 脚本 - 实现 Excalidraw 与 BookxNote 的联动

![](https://cdn.pkmer.cn/images/202312201525630.gif!pkmer)

## 脚本的制作思路

[[Quicker动作之BookxNote和Obsidian联动]] 后,我发布了一篇 [[自定义Excalidraw脚本-实现Zotero与Excalidraw的拖拽联动]] 的文章和视频,是通过 Excalidraw 的脚本实现 Zotero 和 Obsidian 的 Excalidraw 的拖拽无缝连接,效果还不错。

按理说 BookxnotePro 也可以直接通过 Excalidraw 脚本来实现,但一直拖着没去实现,主要 Bookxnote 的数据存储有一点绕,比如这是一个 BookxnotePro 的外部链接:

```text
bookxnotepro://opennote/?nb={ede53699-7b48-4be3-b42c-4aa08bd30ce1}&book=5bde5735bd629aef922f076220efd624&page=49&x=198&y=551&id=181&uuid=0b87d2be8c8b85663d2fd23264e4c866
```

- nb:`{ede53699-7b48-4be3-b42c-4aa08bd30ce1}`
- book:`5bde5735bd629aef922f076220efd624`
- uuid:`0b87d2be8c8b85663d2fd23264e4c866`

包含了 `nb``book``uuid` 的 3 个信息 ID,让我们简单看看这 3 个信息 ID 分别对应什么。

首先找到 Bookxnote 的笔记数据目录所在的文件夹,在 Bookxnote 选项的基本设置里面:

![](https://cdn.pkmer.cn/images/202312201525621.png!pkmer)

打开可以发现里面有几个数据文件,都是用来存储 Bookxnote 软件设置的文件,有最近阅读记录 recentnotes.json 和用户设置 user_config.json。

![](https://cdn.pkmer.cn/images/202312201525622.png!pkmer)

其中 notebooks 文件夹才是我们这次主要关注的对象,也是所有书本笔记数据存储位置,里面有许多的 PDF 对应的文件夹,还有 3 个 JSON 数据文件,manifest.json 里面就存储着所有数据的基本信息:

![](https://cdn.pkmer.cn/images/202312201525623.png!pkmer)

在里面我们就可以找到我们 nb 对应的 ID 值:

![](https://cdn.pkmer.cn/images/202312201525625.png!pkmer)

有 nb 的 ID 值我们可以找到 `entry` 就是 notebook 文件下方对应书籍的文件夹名称,即我们可以从 Bookxnote 的笔记目录数据通过 nb 查找到 PDF 对应的文件夹名称从而推出对应的**笔记数据文件夹**

![](https://cdn.pkmer.cn/images/202312201525626.png!pkmer)

这里面 markups.json 文件就存储所有在 PDF 标注的信息,可以通过 uuid 来找到对应的值:

![](https://cdn.pkmer.cn/images/202312201525627.png!pkmer)

可以发现图片标注和文字标注的结构是不一样的,主要区别就是 `imgfile`

`originaltext` 的信息的不同,可以通过是否存在 `imgfile` 来判断标注信息类型,`fillcolor` 信息可以用来给复制过来的文本添加背景颜色。

`originaltext` 则是标注的原文的文本信息,`imgfile` 对应的值就是图片信息,也就是在**笔记数据文件夹**下的 **imgfiles**文件夹里面存储的图片对应的图片名:

![](https://cdn.pkmer.cn/images/202312201525628.png!pkmer)

### 整理思路 + 代码实现

根据 BookxnotePro 的**外部回链**获取 `nb`, `book`, `uuid` 值,通过读取**笔记数据目录****notebooks** 文件夹的 **manifest.json** 匹配 `nb``id` 值定位 PDF 的文件夹名,再读取 PDF 文件夹名的 `markups.json` 文件通过 uuid 定位获取标注信息。

#### 通过正则正则匹配来匹配外部回链的 nb, book, uuid 值

```js
function matchBookxnoteProRegex(backlink) {
const regex = /nb=({[0-9a-z\-]+})&book=([0-9a-z\-]+).*&uuid=([0-9a-z\-]+)/;

const matches = backlink.match(regex);
if (matches) {
const nb = matches[1];
const book = matches[2];
const uuid = matches[3];
return { nb, book, uuid };
} else {
return null;
}
}

```

#### 通过 nb 查找对应的 book 信息

```js
const fs = require('fs');
const path = require('path');

function findNotebooksById(obj, id) {
if (obj.notebooks && obj.notebooks.length > 0) {
for (let i = 0; i < obj.notebooks.length; i++) {
if (obj.notebooks[i].id === id) {
return obj.notebooks[i];
} else {
const result = findNotebooksById(obj.notebooks[i], id);
if (result) {
return result;
}
}
}
}
return null;
}

// 示例用法
const notebooksData = "D:/PandaNotes/X-图书文件存储/笔记数据/notebooks/manifest.json";
const notebooksJson = JSON.parse(fs.readFileSync(notebooksData, 'utf8'));

const targetId = "{ede53699-7b48-4be3-b42c-4aa08bd30ce1}";
const notebooks = findNotebooksById(notebooksJson, targetId);
console.log(notebooks);

```

#### 通过 uuid 匹配到标注信息

```js
const fs = require('fs');
const path = require('path');

function findMarkupsByUuid(obj, uuid) {
if (Array.isArray(obj.markups)) {
for (let i = 0; i < obj.markups.length; i++) {
if (obj.markups[i].uuid === uuid) {
return obj.markups[i];
}
const markupData = findMarkupsByUuid(obj.markups[i], uuid);
if (markupData) {
return markupData;
}
}
}

return null;
}


// 示例用法
const pdfBooknoteData = "D:/PandaNotes/X-图书文件存储/笔记数据/notebooks/JavaScript高级程序设计(第4版)/markups.json";
const markupJson = JSON.parse(fs.readFileSync(pdfBooknoteData, 'utf8'));

const targetUuid = "0b87d2be8c8b85663d2fd23264e4c866";
const markupData = findMarkupsByUuid(markupJson, targetUuid);
console.log(markupData);
```

## Excalidraw 脚本设置

![](https://cdn.pkmer.cn/images/202312201525629.png!pkmer)

实现由**外部回链**,通过 `Ctrl + V` 粘贴动作获取标注信息到 Excalidraw,附带返回的链接。

## Excalidraw 脚本完整代码

```js
/*
```javascript
*/
const eaApi = ExcalidrawAutomate;
let settings = ea.getScriptSettings();
if (!settings["notebooksPath"]) settings["notebooksPath"] = { value: false };
if (!settings["notebooksPath"].value) {
new Notice("🔴请配置Zotero的Library路径和其他相关设置!", 2000);
settings = {
"notebooksPath": {
value: "D:/PandaNotes/X-图书文件存储/笔记数据",
description: "BookxNotePro的笔记数据文件夹<br>🔴注意路径符号需要转义"
},
"copyBookxnoteImageToObsidian": {
value: true,
description: "是否复制Bookxnote的图片到Obsidian库内<br>🔴注意路径符号需要转义<br>🍀注:如果图片本身就存在于库内就可以关闭该选项"
},
"notebooksImagesPath": {
value: "BookxNotesImages",
description: "Obsidian文件夹,用于存放Bookxnote复制过来的标注图片,请用相对于库的路径"
},
};
ea.setScriptSettings(settings);
} else {
new Notice("✅BookxnoteproToExcalidraw脚本已启动!");
}

// let api = ea.getExcalidrawAPI();
const fs = require('fs');
// const path = require('path');

// 获取bookxnotePro的笔记数据文件夹
const notebookFolder = `${settings["notebooksPath"].value}/notebooks`;

// 获取notebooksImages的存储路径
const basePath = (app.vault.adapter).getBasePath();
const notebooksImagesPath = `${basePath}/${settings["notebooksImagesPath"].value}`;
// 检查文件夹是否存在
if (!fs.existsSync(notebooksImagesPath)) {
// 创建文件夹
fs.mkdirSync(notebooksImagesPath);
console.log('文件夹已创建');
} else {
console.log('文件夹已存在');
}

// 读取manifest.json数据
const notebooksData = `${notebookFolder}/manifest.json`;

const notebooksJson = JSON.parse(fs.readFileSync(notebooksData, 'utf8'));
// console.log(notebooksJson)

eaApi.onPasteHook = async function ({ ea,
payload,
event,
excalidrawFile,
view,
pointerPosition
}) {
console.log("onPaste");
event.preventDefault();
const backlink = payload.text;
console.log(payload.text);
if (payload?.text?.startsWith('bookxnotepro://opennote')) {
console.log("匹配成功");
// 清空原本投入的文本
event.stopPropagation();
payload.text = "";
ea.clear();
ea.style.fillStyle = 'solid';
ea.style.roughness = 0;
// ea.style.roundness = { type: 3 }; // 圆角
ea.style.strokeWidth = 2;
ea.style.fontFamily = 4;
ea.style.fontSize = 20;

// 匹配外部回链对应的信息
const { nb, book, uuid } = matchBookxnoteProRegex(backlink);
console.log({ nb, book, uuid });

// 通过nb找到对应的书本信息
const notebooks = findNotebooksById(notebooksJson, nb);
console.log(notebooks);

// 获取当前书籍的markups.json文件:
const booknoteMarkupsPath = `${notebookFolder}/${notebooks.entry}/markups.json`;
console.log(booknoteMarkupsPath);
const markupsJson = JSON.parse(fs.readFileSync(booknoteMarkupsPath, 'utf8'));
// console.log(markupsJson)

// 获取连接的标注信息
const markupData = findMarkupsByUuid(markupsJson, uuid);
// console.log(markupData?.originaltext);

if (markupData?.imgfile) {
console.log("图片标注");
const imgfilePath = `${notebookFolder}/${notebooks.entry}/imgfiles/${markupData.imgfile}`;
const imgName = `bxn_${markupData.imgfile}`;
// 复制图片到Obsidian的笔记库
if (settings["copyBookxnoteImageToObsidian"].value) {
fs.copyFileSync(imgfilePath, `${notebooksImagesPath}/${imgName}`);
await new Promise((resolve) => setTimeout(resolve, 200)); // 暂停0.2秒,等待复制文件的过程
}

let id = await ea.addImage(0, 0, imgName);
let el = ea.getElement(id);
el.link = backlink;
ea.setView("active");
await ea.addElementsToView(true, false, false);
} else if (markupData?.originaltext) {
console.log("文字标注");

const fillcolor =`#${markupData.fillcolor.slice(2)}`;
ea.style.backgroundColor = fillcolor;
ea.style.strokeColor = "#1e1e1e";

const markupText = processText(markupData.originaltext);
console.log(markupText);
let id = await ea.addText(0, 0, `${markupText}`, { width: 600, box: true, wrapAt: 90, textAlign: "left", textVerticalAlign: "middle", box: "box" });
let el = ea.getElement(id);
el.link = backlink;
ea.setView("active");
await ea.addElementsToView(true, false, false);
} else {
new Notice("未匹配到标注信息,请重新标注或者手动插入");
}

return false;
}
// return true;
};

function matchBookxnoteProRegex(backlink) {
const regex = /nb=({[0-9a-z\-]+})&book=([0-9a-z\-]+).*&uuid=([0-9a-z\-]+)/;

const matches = backlink.match(regex);
if (matches) {
const nb = matches[1];
const book = matches[2];
const uuid = matches[3];
return { nb, book, uuid };
} else {
return null;
}
}

function findNotebooksById(obj, id) {
if (obj.notebooks && obj.notebooks.length > 0) {
for (let i = 0; i < obj.notebooks.length; i++) {
if (obj.notebooks[i].id === id) {
return obj.notebooks[i];
} else {
const result = findNotebooksById(obj.notebooks[i], id);
if (result) {
return result;
}
}
}
}
return null;
}

function findMarkupsByUuid(obj, uuid) {
if (Array.isArray(obj.markups)) {
for (let i = 0; i < obj.markups.length; i++) {
if (obj.markups[i].uuid === uuid) {
return obj.markups[i];
}
const markupData = findMarkupsByUuid(obj.markups[i], uuid);
if (markupData) {
return markupData;
}
}
}

return null;
}

function processText(text) {
// 替换特殊空格为普通空格
text = text.replace(/[\ue5d2\u00a0\u2007\u202F\u3000\u314F\u316D\ue5cf]/g, ' ');
// 将全角字符转换为半角字符
text = text.replace(/[\uFF01-\uFF5E]/g, function (match) { return String.fromCharCode(match.charCodeAt(0) - 65248); });
// 替换英文之间的多个空格为一个空格
text = text.replace(/([a-zA-Z])([\u4e00-\u9fa5])/g, '$1 $2');

// 删除中文之间的空格
text = text.replace(/([0-9\.\u4e00-\u9fa5])\s+([0-9\.\u4e00-\u9fa5])/g, '$1$2');
text = text.replace(/([0-9\.\u4e00-\u9fa5])\s+([0-9\.\u4e00-\u9fa5])/g, '$1$2');
text = text.replace(/([\u4e00-\u9fa5])\s+/g, '$1');
text = text.replace(/\s+([\u4e00-\u9fa5])/g, '$1');

// // 在中英文之间添加空格
// text = text.replace(/([\u4e00-\u9fa5])([a-zA-Z])/g, '$1 $2');
// text = text.replace(/([a-zA-Z])([\u4e00-\u9fa5])/g, '$1 $2');

return text;
}
```
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
---
uid: 20231207020538
title: Excalidraw 自定义脚本 - 插入可以编辑的 Mermaid 图形
title: 自定义 Excalidraw 脚本 - 插入可以编辑的 Mermaid 图形
tags: [Excalidraw, Excalidraw脚本, Mermaid]
description: Excalidraw 插入 Mermaid 后就不能对该 Mermaid 进行源码编辑,为此可以换个思路添加 Mermaid 图形,那就是将插入的 Mermaid 转为 SVG 格式,且 Ctrl + 鼠标左键单击 可以弹出源码进行二次编辑。
author: 一鸣惊人,熊猫别熬夜
type: other
draft: false
editable: false
modified: 20231207021206
modified: 20231220153026
---

# Excalidraw 自定义脚本 - 插入可以编辑的 Mermaid 图形
# 自定义 Excalidraw 脚本 - 插入可以编辑的 Mermaid 图形

Excalidraw 插入 Mermaid 后就不能对该 Mermaid 进行源码编辑,为此可以换个思路添加 Mermaid 图形,那就是将插入的 Mermaid 转为 SVG 格式,且 `Ctrl + 鼠标左键单击` 可以弹出源码进行二次编辑。

Expand Down

0 comments on commit e843e1a

Please sign in to comment.