自定义 Excalidraw 脚本:实现 Zotero 与 Excalidraw 的拖拽联动

之前分享过 Quicker动作之BookxNote和Obsidian联动 实现了 Excalidraw 画板与 BookxNote 的联动,需要借助 Quicker 来点击 2 次才能完成一个过程,整体下来还是比较麻烦的(不过我自己还是经常在用),这次将介绍 Zotero 与 Excalidraw 的无缝连接,不需要借助 Quicker,而是通过自定义 Excalidraw 的脚本,直接拖拽 Zotero 的文本或者图片就可以实现 Zotero 到 Excalidraw 的笔记了,拖拽过来的包含注释和选择的内容


这是一个从 https://pkmer.cn/pkmer-docs/10-obsidian/obsidian%e7%a4%be%e5%8c%ba%e6%8f%92%e4%bb%b6/excalidraw/%e8%87%aa%e5%ae%9a%e4%b9%89excalidraw%e8%84%9a%e6%9c%ac-%e5%ae%9e%e7%8e%b0zotero%e4%b8%8eexcalidraw%e7%9a%84%e6%8b%96%e6%8b%bd%e8%81%94%e5%8a%a8 下的原始话题分离的讨论话题

那步骤设置后,拖拽到excalidraw后,只有文章名,未显示具体的拖拽内容,请问怎么回事?

如果你能将你的设置部分截图,如下所示,我才可以更加清楚的了解你遇到问题

作者大大你好,我在zotero升级到7之后,在zotero中做好标记,图片可以正常拖拽进Excalidraw中,文字部分拖拽进去后,不会在正中间显示,会显示在视图外,要把屏幕缩小才能找到

感谢反馈,已修复,更新下就好了(见下源码),或者通过脚本安装市场PKMer_Excalidraw Script Install Market:轻松管理和获取 Excalidraw 脚本来更新。

源码:

24.10.10 修复Zotero文本错位

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则为白底黑字)");
}
const eaApi = ExcalidrawAutomate;



eaApi.onDropHook = async function ({ ea, payload, event, pointerPosition }) {
	console.log("ondrop");
	event.preventDefault();
	var insert_txt = event.dataTransfer.getData("Text");
	const ondropType = event.dataTransfer.files.length;
	console.log(ondropType);


	if (insert_txt.includes("zotero://")) {
		// 格式化文本(去空格、全角转半角)  
		insert_txt = processText(insert_txt);
		// 清空原本投入的文本
		event.stopPropagation();
		console.log("✔Zotero ondrop");
		console.log(pointerPosition);
		await processZoteroData(ea, insert_txt, pointerPosition);

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

eaApi.onPasteHook = async function ({ ea, payload, event, excalidrawFile, view, pointerPosition }) {
	console.log("onPaste");
	event.preventDefault();
	const inputText = payload.text;

	if (payload?.text?.includes('zotero://')) {
		console.log("匹配成功");
		// 清空原本投入的文本
		event.stopPropagation();
		payload.text = "";
		insert_txt = processText(inputText);
		event.stopPropagation();
		console.log("✔Zotero onPasteHook");
		console.log(pointerPosition);
		await processZoteroData(ea, insert_txt, pointerPosition);
	}
};

async function processZoteroData(ea, insert_txt, pointerPosition) {
        ea.setView("active");
	ea.clear();
	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 = 1;
	ea.style.fontFamily = 4;
	ea.style.fontSize = 20;

	let zotero_color = match_zotero_color(insert_txt);
	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})`;
	} else {
		zotero_author = `(Zotero)`;
	}

	zotero_comment = match_zotero_comment(insert_txt);
	if (zotero_comment) {
		zotero_comment = `\n\n${zotero_comment}`;
	}

	if (zotero_txt) {
		console.log("ZoteroText");
		let id = await ea.addText(
			null,
			null,
			`${zotero_txt}${zotero_author}${zotero_comment}`,
			{
				width: 400,
				box: true,
				wrapAt: 99,
				textAlign: "left",
				textVerticalAlign: "middle",
				box: "box"
			}
		);
		let el = ea.getElement(id);
		el.link = `[${zotero_author}](${zotero_link})`;
		el.height = el.height - 100;
		// 计算中心位置
		el.x = pointerPosition?.x - (el.width / 2);
		el.y = pointerPosition?.y - (el.height / 2);
		await ea.addElementsToView(false, true, false);


	} 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}`;
		if (!fs.existsSync(`${basePath}/${relativePath}`)) {
			fs.mkdirSync(path.dirname(`${basePath}/${relativePath}`), { recursive: true });
		}
		let zotero_image_path = `${zotero_library_path}/${zotero_image_name}`;
		fs.copyFileSync(zotero_image_path, Obsidian_image_Path);
		let id = await ea.addImage(null, null, zotero_image_name);
		let el = ea.getElement(id);
		el.link = `[${zotero_author}](${zotero_link})`;
		await new Promise((resolve) => setTimeout(resolve, 200));
		await ea.addElementsToView(true, true, false);

	}
}


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].replace("page=NaN&", "") : "";
}

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] : "";
}