自定义 Excalidraw 脚本:实现 Zotero 与 Excalidraw 的拖拽联动
概述
之前分享过 Quicker动作之BookxNote和Obsidian联动 实现了 Excalidraw 画板与 BookxNote 的联动,需要借助 Quicker 来点击 2 次才能完成一个过程,整体下来还是比较麻烦的(不过我自己还是经常在用),这次将介绍 Zotero 与 Excalidraw 的无缝连接,不需要借助 Quicker,而是通过自定义 Excalidraw 的脚本,直接拖拽 Zotero 的文本或者图片就可以实现 Zotero 到 Excalidraw 的笔记了,拖拽过来的包含注释和选择的内容。
PS:Zotero 7 和 Zotero 6 通用。
Zotero 快速复制格式设置
因为这个脚本是根据拖入的文本信息来提取的,所以针对 Zotero 的快速复制格式有一定的设置要求,请取设置如下格式:
注意不要设置为
,这样匹配不到信息。
实现过程
具体实现过程非常简单,第一步修改对应的路径,第二步保存 md 文件到指定文件夹,之后点击运行脚本就完成了。
保存为 md 文件保存到指定文件夹
将下面代码保存为 md 文件,修改放入 Excalidraw 指定的 Scripts 的文件夹中,在 Excalidraw 的画板中的工具面板中就会出现一个齿轮按钮,需要时点击运行。
修改📌注释的文件路径
第一次运行时,需要修改路径。在第一次单击⚙️运行后,在 Excalidraw 插件的设置中会出现以下选项:
注意:需要单击运行后,插件设置才会加载这个选项。
- 第一个为 Zotero 的图形存储路径;
- Zotero 的标注图片一般在你定义的数据库的文件夹的cache->library下面:
- Zotero 的标注图片一般在你定义的数据库的文件夹的cache->library下面:
- 第二个就是你自定义拖拽过来的图片存放的存储路径了,注意要在 Obsidian 的笔记库中的相对路径。
Caution
- 通过文本管理器复制过来的路径格式为:D:\Zotero\cache\library,请务必修改为D:/Zotero/cache/library(注意是/而不是\);
- 再次声明,如果和作者 Zotero Images Paths 配置一样,但是无论怎么插入在📁Y- 图形文件存储中始终看不到📁ZoteroImages的建立,请务必检查 Zotero Library Path 是否和高亮文本的格式一致,路径是否有缺漏的地方;
- 如果 Zotero 只有光秃秃的一个 PDF 或者 Epub 的时候,上述‘折腾’不生效,请创建父条目再试,Zotero 右键菜单有;
- PS:如果连 Zotero 父条目都不知道,请跳转 Zotero 专区来学习。
- 如果配置无误的情况下,请重启 Obsidian,重新插入试试;
- 如果确定自己配置不存在问题却依旧出现问题,请截图并说明自己存在的问题之后在群里再@。
脚本安装
脚本安装可以根据源码来安装,也可以通过 Excalidraw 插件提供的脚本安装代码块来安装
- 代码块链接方法:
- 优点:一键安装脚本和图标,操作方便,后续脚本更新可以检测
- 缺点:国内需要可访问 GitHub 的网络
- 源码拷贝方式:
- 优点:不需要特殊网络
- 缺点:需要手动复制源码,这个过程很容易出问题,没有图标,脚本更新无法检测…
PS:之后我的脚本更新或者 BUG 修复,可能不会更新到网站,而是直接更新到 GitHub,因为这样对我来说比较方便点而且快速点。
代码块链接方法
```excalidraw-script-install
https://raw.githubusercontent.com/PandaNocturne/ExcalidrawScripts/master/PandaScripts/ZoteroToExcalidraw.md
```
将上述代码块放到一个 md 文件,阅读模式下该代码块会显示为 安装脚本的按钮,点击安装之后,更新为脚本已是最新 - 点击重新安装,即当前脚本已经安装,在 Excalidraw 画布界面的侧边Obsidian 工具面板中可以查看。
源码拷贝方式
let settings = ea.getScriptSettings();
//set default values on first run
if (!settings["Zotero Library Path"]) settings["Zotero Library Path"] = { value: false };
if (!settings["Zotero Library Path"].value) {
new Notice("🔴请配置Zotero的Library路径和其他相关设置!", 2000);
settings = {
"Zotero Library Path": {
value: "D:/Zotero/cache/library",
description: "Zotero Library的路径,比如:D:/Zotero/cache/library"
},
"Zotero Images Path": {
value: "Y-图形文件存储/ZoteroImages",
description: "Obsidian库内存放Zotero的图片的相对路径,比如:Y-图形文件存储/ZoteroImages"
},
"Zotero Annotations Color": {
value: false,
description: "是否开启匹配Zotero的颜色选项栏<br>❗注:匹配颜色选项需要修改Zotero的高亮标注模板",
},
};
ea.setScriptSettings(settings);
} else {
new Notice("✅ZoteroToExcalidraw脚本已启动!");
}
const path = require('path');
const fs = require("fs");
// 获取库的基本路径
const basePath = (app.vault.adapter).getBasePath();
// 📌修改到Zotero的library文件夹
const zotero_library_path = settings["Zotero Library Path"].value;
// 设置相对路径
const relativePath = settings["Zotero Images Path"].value;
// let api = ea.getExcalidrawAPI();
let el = ea.targetView.containerEl.querySelectorAll(".excalidraw-wrapper")[0];
let InsertStyle;
if (settings["Zotero Annotations Color"].value) {
const fillStyles = ["文字", "背景"];
InsertStyle = await utils.suggester(fillStyles, fillStyles, "选择插入卡片颜色的形式,ESC则为白底黑字)");
}
el.ondrop = async function (event) {
console.log("ondrop");
event.preventDefault();
var insert_txt = event.dataTransfer.getData("Text");
const ondropType = event.dataTransfer.files.length;
console.log(ondropType);
// 设定一些样式
ea.style.strokeStyle = "solid";
ea.style.fillStyle = 'solid';
ea.style.roughness = 0;
ea.style.backgroundColor = "transparent";
ea.style.strokeColor = "#1e1e1e";
// ea.style.roundness = { type: 3 }; // 圆角
ea.style.strokeWidth = 2;
ea.style.fontFamily = 4;
ea.style.fontSize = 20;
if (insert_txt.includes("zotero://")) {
// 格式化文本(去空格、全角转半角)
insert_txt = processText(insert_txt);
// 清空原本投入的文本
event.stopPropagation();
ea.clear();
console.log("Zotero");
let zotero_color = match_zotero_color(insert_txt);
// alert(zotero_color);
if (zotero_color) {
// 卡片背景颜色
if (InsertStyle == "背景") {
ea.style.backgroundColor = zotero_color;
ea.style.strokeColor = "#1e1e1e";
} else if (InsertStyle == "文字") {
ea.style.backgroundColor = "#ffffff";
ea.style.strokeColor = zotero_color;
} else {
ea.style.backgroundColor = "transparent";
ea.style.strokeColor = "#1e1e1e";
}
} else {
ea.style.backgroundColor = "transparent";
ea.style.strokeColor = "#1e1e1e";
}
zotero_txt = match_zotero_txt(insert_txt);
zotero_author = match_zotero_author(insert_txt);
zotero_link = match_zotero_link(insert_txt);
if (zotero_author) {
zotero_author = `[(${zotero_author})](${zotero_link})`;
};
zotero_comment = match_zotero_comment(insert_txt);
if (zotero_comment) {
zotero_comment = `\n\n${zotero_comment}`;
};
if (zotero_txt) {
console.log("ZoteroText");
const totalText = `${zotero_txt}${zotero_comment}`;
let width = totalText.length > 30 ? 600 : totalText.length * 20;
let id = await ea.addText(0, 0, `${zotero_txt}${zotero_author}${zotero_comment}`, { width: width, box: true, wrapAt:99, textAlign: "left", textVerticalAlign: "middle", box: "box" });
let el = ea.getElement(id);
// el.link = zotero_link;
await ea.addElementsToView(true, true, false);
if (ea.targetView.draginfoDiv) {
document.body.removeChild(ea.targetView.draginfoDiv);
delete ea.targetView.draginfoDiv;
};
} else {
console.log("ZoteroImage");
let zotero_image = match_zotero_image(insert_txt);
let zotero_image_name = `${zotero_image}.png`;
let Obsidian_image_Path = `${basePath}/${relativePath}/${zotero_image_name}`;
// 如果Ob的路径不存在则创建
if (!fs.existsSync(`${basePath}/${relativePath}`)) {
fs.mkdirSync(path.dirname(`${basePath}/${relativePath}`), { recursive: true });
}
let zotero_image_path = `${zotero_library_path}/${zotero_image_name}`;
// 复制zotero的图片到Obsidian的笔记库
fs.copyFileSync(zotero_image_path, Obsidian_image_Path);
await new Promise((resolve) => setTimeout(resolve, 200)); // 暂停0.2秒,等待复制文件的过程
let id = await ea.addImage(0, 0, zotero_image_name);
let el = ea.getElement(id);
el.link = zotero_author;
await ea.addElementsToView(true, true, false);
if (ea.targetView.draginfoDiv) {
document.body.removeChild(ea.targetView.draginfoDiv);
delete ea.targetView.draginfoDiv;
};
};
} else if (ondropType < 1) {
// 清空原本投入的文本
event.stopPropagation();
ea.clear();
// 格式化文本(去空格、全角转半角)
insert_txt = processText(insert_txt);
console.log("文本格式化");
let width = insert_txt.length > 30 ? 600 : insert_txt.length * 15;
await ea.addText(0, 0, `${insert_txt} `, { width: width, box: true, wrapAt: 90, textAlign: "left", textVerticalAlign: "middle", box: "box" });
// let el = ea.getElement(id);
await ea.addElementsToView(true, true, false);
if (ea.targetView.draginfoDiv) {
document.body.removeChild(ea.targetView.draginfoDiv);
delete ea.targetView.draginfoDiv;
};
};
};
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;
}
function match_zotero_color(text) {
const regex = /#[a-zA-Z0-9]{6}/;
const matches = text.match(regex);
return matches ? matches[0] : "";
}
function match_zotero_txt(text) {
const regex = /“(.*)” \(/;
const matches = text.match(regex);
return matches ? matches[1] : "";
}
function match_zotero_author(text) {
const regex = /\(\[(.*\d+)]\(/;
const matches = text.match(regex);
return matches ? matches[1] : "";
}
function match_zotero_link(text) {
const regex = /\[pdf\]\((.*)\)\)/;
const matches = text.match(regex);
return matches ? matches[1] : "";
}
function match_zotero_comment(text) {
const regex = /.*\)\).*\)\)([\s\S]*)/;
const matches = text.match(regex);
return matches ? matches[1] : "";
}
function match_zotero_image(text) {
const regex = /annotation=(\w*)/;
const matches = text.match(regex);
return matches ? matches[1] : "";
}
该脚本的中心思想就是通过拖拽的文本,定位到图片名,从而复制该图片到 OB 的笔记库中,并对拖拽的文本进行处理,去除多余的空格以及全角转半角,拆分为 zotero_txt、zotero_author、zotero_link、zotero_comment、zotero_image 这 5 个文本,自定义组合:
- 如果拖拽文本包含 “zotero://” 则进行拆分组合,否则只对文本进行格式化处理
- 判断 zotero_txt 是否包含文本?
- True:为文本标注,组合文本为需要的格式
- 判断 zotero_comment、zotero_author 是否包含文本?
- True:包含文本,即添加
- False:为空值
- 组合 zotero_txt、zotero_author、zotero_comment
- 添加组合文本并带有回链 zotero_link 到 Excalidraw 画板
- 判断 zotero_comment、zotero_author 是否包含文本?
- False:为图片标注
- 通过 match_zotero_image 来图片名 zotero_image
- 复制 Zotero 的图片到指定文件夹
- 等待 0.2 秒来复制文件
- 复制 Zotero 的图片到指定文件夹
- 添加图片并带有回链 zotero_link 到 Excalidraw 画板
- 通过 match_zotero_image 来图片名 zotero_image
- True:为文本标注,组合文本为需要的格式
- 判断 zotero_txt 是否包含文本?
如果你是拖拽替他文本,该脚本可以帮你格式化处理一下的
进一步配置:添加卡片颜色
下述内容请自行折腾
Tip如果你需要设定颜色可以取消这一行的注释:
// let InsertStyle = await utils.suggester(fillStyles, fillStyles, "选择插入卡片颜色的形式,ESC退出(默认白底黑字)");
取消后再点击⚙️按钮会出现如下弹窗
如果你想添加标注的卡片颜色,可以在首选项 ->高级中编辑器 ->搜索:annotations.noteTemplates
,修改高亮标注的模板,添加{{color}}属性,其他属性见官方文档:note templates [Zotero Documentation]:
<p>{{color}} {{highlight}} {{citation}} {{comment}}</p>
不要随意修改 NoteTemplate
因为该脚本是通过文本正则匹配的,所以当你修改了高亮的模板时,这个脚本可能会失效,如果你想匹配自己的模板,可以自行修改匹配条件。
设置完成之后,再运行该脚本,插入的卡片就可以根据匹配标注的颜色了,根据选择可以设置 2 种卡片颜色方案:
Cite
视频教程
讨论
若阁下有独到的见解或新颖的想法,诚邀您在文章下方留言,与大家共同探讨。
反馈交流
其他渠道
版权声明
版权声明:所有 PKMer 文章如果需要转载,请附上原文出处链接。