--- name: 粘贴摘录解析引擎 description: 后台引擎:拦截并解析粘贴板中的特殊格式(BookxNote、Zotero、Eagle、Bookmaster及自定义ymjr等)为带链接的 Excalidraw 元素。 author: ymjr version: 1.0.0 license: MIT usage: 后台常驻。当您在画板中按下 Ctrl+V 粘贴时,自动探测文本剪贴板中是否包含支持的第三方软件协议链接。如果命中,会自动转换为对应的图片、引用块或代码块元素。 features: - 拦截 `onPaste` 钩子,支持 BookxNote Pro, Zotero PDF, Bookmaster, Booknote, Eagle 格式。 - 支持私有的 `ymjr:` JSON 协议,实现富文本与代码高亮等高级粘贴机制。 - 智能清洗由于跨软件复制带来的无效 HTML 富文本标签。 dependencies: - 独立运行。如果是代码高亮协议,需配合 [CodeHighlight] 模块使用。 autorun: true --- /* ```javascript */ var locales = { zh: { log_parse_error: "[Feature: PasteExtractor] 解析 ymjr JSON 错误:", log_no_core: "EA_Core 未运行,无法挂载摘录引擎", log_unmounted: "[{id}] 🔌 已卸载", log_mounted: "[{id}] 🚀 挂载完毕" }, en: { log_parse_error: "[Feature: PasteExtractor] Error parsing ymjr JSON:", log_no_core: "EA_Core not running, cannot mount paste extractor engine", log_unmounted: "[{id}] 🔌 Unmounted", log_mounted: "[{id}] 🚀 Mounted successfully" } }; const SCRIPT_ID = "ymjr.feature.paste-engine"; // ========================================== // 核心逻辑:粘贴事件拦截与解析 // ========================================== const handlePaste = async (contextPayload) => { const { ea, api, payload, event } = contextPayload; if (!ea || !api) return; if (!payload || typeof payload.text !== "string") { return false; } const state = api.getAppState(); let text = payload.text; // ------------------------------------------ // 1. 全局清理:剥离富文本 HTML 标签 // ------------------------------------------ if (text.includes('/g, '').replaceAll('', ''); } if (text.includes('/g, '').replaceAll('', ''); } if (text.includes('/g, '').replaceAll('', ''); } // 同步修改回 payload payload.text = text; // ------------------------------------------ // 2. 解析:BookxNote Pro // ------------------------------------------ if (text.includes('(bookxnotepro://')) { let image_paths = text.match(/!\[\[.*\]\]/g); if (image_paths?.length) { contextPayload.intercepted = true; // 同步拦截信号 let links = text.match(/bookxnotepro:\/\/[^\)]*/g); // 处理引用块文本 if (text.startsWith("```ad-quote")) { const match = text.match(/^```ad-quote\s*([\w\W]*)\s```/); if (match) { payload.text = ''; ea.style.strokeColor = state.currentItemStrokeColor; ea.style.fontSize = state.currentItemFontSize; ea.style.fontFamily = state.currentItemFontFamily; const id = ea.addText(0, 0, match[1]); const el = ea.getElement(id); if (links && links[0]) el.link = links[0]; await ea.addElementsToView(true, false, false); return true; } } // 处理图片 payload.text = ''; ea.clear(); for (let i = 0; i < image_paths.length; i++) { let image_path = image_paths[i]; let link = links ? links[i] : null; let id = await ea.addImage(0, 0, image_path.substring(3, image_path.length - 2), true); let el = ea.getElement(id); if (link) el.link = link; } await ea.addElementsToView(true, false, false); return true; } } // ------------------------------------------ // 3. 解析:Zotero PDF // ------------------------------------------ if (text.includes('[pdf](zotero://')) { let image_paths = text.match(/!\[\[.*\]\]/g); if (image_paths?.length) { contextPayload.intercepted = true; // 同步拦截信号 payload.text = ''; let links = text.match(/zotero:\/\/[^\)]*/g); ea.clear(); let y = 0; for (let i = 0; i < image_paths.length; i++) { let image_path = image_paths[i]; let link = links ? links[i] : null; let id = await ea.addImage(0, y, image_path.substring(3, image_path.length - 2), false); let el = ea.getElement(id); if (link) el.link = link; y += el.height + 10; } await ea.addElementsToView(true, false, false); return true; } } // ------------------------------------------ // 4. 解析:Bookmaster // ------------------------------------------ if (text.includes('obsidian://bookmaster')) { let image_paths = text.match(/!\[\[.*\]\]/g); if (image_paths && image_paths.length > 0) { contextPayload.intercepted = true; // 同步拦截信号 payload.text = ''; let links = text.match(/obsidian:\/\/bookmaster[^\)]*/g); ea.clear(); let y = 0; for (let i = 0; i < image_paths.length; i++) { let image_path = image_paths[i]; let link = links ? links[i] : null; let id = await ea.addImage(0, y, image_path.substring(3, image_path.length - 2), true); let el = ea.getElement(id); if (link) el.link = link; el.width /= 4; el.height /= 4; y += el.height + 1; } await ea.addElementsToView(true, false, false); return true; } } // ------------------------------------------ // 5. 解析:Booknote // ------------------------------------------ if (text.includes('obsidian://booknote')) { let image_paths = text.match(/!\[\[.*\]\]/g); if (image_paths && image_paths.length > 0) { contextPayload.intercepted = true; // 同步拦截信号 payload.text = ''; let links = text.match(/obsidian:\/\/booknote[^\)]*/g); ea.clear(); let y = 0; for (let i = 0; i < image_paths.length; i++) { let image_path = image_paths[i]; let link = links ? links[i] : null; let id = await ea.addImage(0, y, image_path.substring(3, image_path.length - 2), true); let el = ea.getElement(id); if (link) el.link = link; el.width /= 4; el.height /= 4; y += el.height + 1; } await ea.addElementsToView(true, false, false); return true; } } // ------------------------------------------ // 6. 解析:Eagle 链接 // ------------------------------------------ if (text.match(/!\[(.*?)\]\((.*?)\)/)) { const match = text.match(/!\[(.*?)\]\((.*?)\)/); if (match) { contextPayload.intercepted = true; // 同步拦截信号 payload.text = ''; ea.clear(); const id = await ea.addImage(0, 0, match[2], true); const el = ea.getElement(id); const linkMatch = text.match(/.*(eagle:\/\/item.*)\)/); if (linkMatch) el.link = linkMatch[1]; await ea.addElementsToView(true, false, false); return true; } } // ------------------------------------------ // 7. 解析:YMJR 自定义协议 // ------------------------------------------ if (text.startsWith("ymjr:")) { // 【关键修复点】:通知 Core 取消原生粘贴动作,并在进入异步执行前立刻清空剪贴板缓存 contextPayload.intercepted = true; payload.text = ''; if (event && typeof event.preventDefault === "function") { event.preventDefault(); // 直接终止原生 DOM 的 Paste 事件 } const jsonStr = text.substring("ymjr:".length); ea.clear(); try { const obj = JSON.parse(jsonStr); if (obj?.type === 'image' || obj?.type === 'image_path') { let id; if (obj.type === 'image') { const file = await ea.targetView.excalidrawData.saveDataURLtoVault(obj.data, "image/png", ""); id = await ea.addImage(0, 0, file, true); } else if (obj.type === 'image_path') { id = await ea.addImage(0, 0, `file:///${obj.data}`, true); } const el = ea.getElement(id); el.link = obj.link; await ea.addElementsToView(true, false, false); return true; } else if (obj?.type === 'text') { ea.style.strokeColor = state.currentItemStrokeColor; ea.style.fontSize = state.currentItemFontSize; ea.style.fontFamily = state.currentItemFontFamily; const id = await ea.addText(0, 0, obj.data, { wrapAt: 100 }); const el = ea.getElement(id); el.link = obj.link; await ea.addElementsToView(true, false, false); return true; } else if (obj?.type === 'code') { const id = await ea.addText(0, 0, obj.text.replaceAll('\r\n', '\n')); const el = ea.getElement(id); el.fontFamily = 3; el.link = `ymjr://open?app=code&folder=${encodeURI(obj.folderPath)}&file=${encodeURI(obj.filePath)}&line=${obj.lineNum}`; el.customData = { codeHighlight: { embedFont: true, language: obj?.language || "Verilog", style: "atom-one-dark" } }; ea.refreshTextElementSize(id); await ea.addElementsToView(true, false, false); return true; } } catch (e) { console.error(t("log_parse_error"), e); } } return false; }; // ========================================== // 生命周期挂载与卸载 // ========================================== async function mountFeature() { if (!window.EA_Core) return console.warn(t("log_no_core")); if (typeof ExcalidrawAutomate.plugin[`disable_${SCRIPT_ID}`] === "function") ExcalidrawAutomate.plugin[`disable_${SCRIPT_ID}`](); // 正常挂载。这里的 dispatchSync 底层已修改,即使 handlePaste 有 await 也能实现同步拦截的传导 window.EA_Core.registerHook(SCRIPT_ID, 'onPaste', handlePaste, 50); ExcalidrawAutomate.plugin[`disable_${SCRIPT_ID}`] = () => { if (window.EA_Core) { window.EA_Core.unregisterHook(SCRIPT_ID); } console.log(t("log_unmounted", { id: SCRIPT_ID })); }; console.log(t("log_mounted", { id: SCRIPT_ID })); } mountFeature();