diff --git a/package-lock.json b/package-lock.json index 66d2179..7d65000 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,7 @@ "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "yamljs": "^0.3.0", - "zotero-plugin-toolkit": "^4.0.8" + "zotero-plugin-toolkit": "^4.0.9" }, "devDependencies": { "@esbuild-plugins/node-globals-polyfill": "^0.2.3", @@ -13588,9 +13588,9 @@ } }, "node_modules/zotero-plugin-toolkit": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/zotero-plugin-toolkit/-/zotero-plugin-toolkit-4.0.8.tgz", - "integrity": "sha512-uro188jzhAY+6EB2N9Kx0Vn5ITazwljJOragMi7PjrcRQrEi/R350eT+MAFrDR+pMzEOltrKtz6W7aSvMd9iqA==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/zotero-plugin-toolkit/-/zotero-plugin-toolkit-4.0.9.tgz", + "integrity": "sha512-DLFB17D4hWAZRLBKHfipZke765StKBlZ1S320cQqducHaEW7dWC1tumBjB8n02DtuVGn6l9YKo4819wbyVMKcQ==", "dependencies": { "zotero-types": "^2.2.0" }, diff --git a/package.json b/package.json index ab7b7dd..81298e6 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "yamljs": "^0.3.0", - "zotero-plugin-toolkit": "^4.0.8" + "zotero-plugin-toolkit": "^4.0.9" }, "devDependencies": { "@esbuild-plugins/node-globals-polyfill": "^0.2.3", diff --git a/src/extras/editor/pasteMarkdown.ts b/src/extras/editor/pasteMarkdown.ts new file mode 100644 index 0000000..06f7c9b --- /dev/null +++ b/src/extras/editor/pasteMarkdown.ts @@ -0,0 +1,132 @@ +import { Plugin, PluginKey } from "prosemirror-state"; +import { md2html } from "../../utils/convert"; + +export { initPasteMarkdownPlugin }; + +declare const _currentEditorInstance: { + _editorCore: EditorCore; +}; + +function initPasteMarkdownPlugin() { + const core = _currentEditorInstance._editorCore; + console.log("Init BN Paste Markdown Plugin"); + const key = new PluginKey("pasteDropPlugin"); + const oldPlugins = core.view.state.plugins; + const oldPastePluginIndex = oldPlugins.findIndex( + (plugin) => plugin.props.handlePaste && plugin.props.handleDrop, + ); + if (oldPastePluginIndex === -1) { + console.error("Paste plugin not found"); + return; + } + const oldPastePlugin = oldPlugins[oldPastePluginIndex]; + const newState = core.view.state.reconfigure({ + plugins: [ + ...oldPlugins.slice(0, oldPastePluginIndex), + new Plugin({ + key, + props: { + handlePaste: (view, event, slice) => { + if (!event.clipboardData) { + return false; + } + const markdown = getMarkdown(event.clipboardData); + + if (!markdown) { + // Try the old paste plugin + return oldPastePlugin.props.handlePaste?.apply(oldPastePlugin, [ + view, + event, + slice, + ]); + } + + md2html(markdown).then((html: string) => { + const slice = window.BetterNotesEditorAPI.getSliceFromHTML( + view.state, + html, + ); + const tr = view.state.tr.replaceSelection(slice); + view.dispatch(tr); + }); + return true; + }, + handleDrop: (view, event, slice, moved) => { + if (!event.dataTransfer) { + return false; + } + + const markdown = getMarkdown(event.dataTransfer); + if (!markdown) { + // Try the old drop plugin first + return oldPastePlugin.props.handleDrop?.apply(oldPastePlugin, [ + view, + event, + slice, + moved, + ]); + } + + md2html(markdown).then((html: string) => { + const slice = window.BetterNotesEditorAPI.getSliceFromHTML( + view.state, + html, + ); + const pos = view.posAtCoords({ + left: event.clientX, + top: event.clientY, + }); + if (!pos) { + return; + } + // Insert the slice to the current position + const tr = view.state.tr.insert(pos.pos, slice); + view.dispatch(tr); + }); + + return true; + }, + }, + }), + ...oldPlugins.slice(oldPastePluginIndex + 1), + ], + }); + core.view.updateState(newState); +} + +function getMarkdown(clipboardData: DataTransfer) { + // If the clipboard contains HTML, don't handle it + if (clipboardData.types.includes("text/html")) { + return false; + } + + if (clipboardData.types.includes("text/markdown")) { + return clipboardData.getData("text/markdown"); + } + + // For Typora + if (clipboardData.types.includes("text/x-markdown")) { + return clipboardData.getData("text/x-markdown"); + } + + // Match markdown patterns + if (clipboardData.types.includes("text/plain")) { + const text = clipboardData.getData("text/plain"); + const markdownPatterns = [ + /^#/m, // Headers: Lines starting with # + /^\s*[-+*]\s/m, // Unordered lists: Lines starting with -, +, or * + /^\d+\.\s/m, // Ordered lists: Lines starting with numbers followed by a dot + /\[.*\]\(.*\)/, // Links: [text](url) + /`[^`]+`/, // Inline code: `code` + /^> /m, // Blockquotes: Lines starting with > + /```/, // Code blocks: Triple backticks + ]; + + for (const pattern of markdownPatterns) { + if (pattern.test(text)) { + return text; + } + } + } + return false; +} diff --git a/src/extras/editorScript.ts b/src/extras/editorScript.ts index 5c913e9..7237a61 100644 --- a/src/extras/editorScript.ts +++ b/src/extras/editorScript.ts @@ -12,6 +12,7 @@ import { import { EditorState, TextSelection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { initLinkPreviewPlugin } from "./editor/linkPreview"; +import { initPasteMarkdownPlugin } from "./editor/pasteMarkdown"; declare const _currentEditorInstance: { _editorCore: EditorCore; @@ -378,6 +379,7 @@ export const BetterNotesEditorAPI = { getNodeFromHTML, setSelection, initLinkPreviewPlugin, + initPasteMarkdownPlugin, }; // @ts-ignore diff --git a/src/utils/editor.ts b/src/utils/editor.ts index 1149490..ebafdfb 100644 --- a/src/utils/editor.ts +++ b/src/utils/editor.ts @@ -452,33 +452,46 @@ function initLinkPreview(editor: Zotero.EditorInstance) { return; } const EditorAPI = getEditorAPI(editor); - EditorAPI.initLinkPreviewPlugin( - Components.utils.cloneInto( - { - setPreviewContent: ( - link: string, - setContent: (content: string) => void, - ) => { - const note = addon.api.convert.link2note(link); - if (!note) { - setContent(`

Invalid note link: ${link}

`); - return; - } - addon.api.convert - .link2html(link, { - noteItem: note, - dryRun: true, - usePosition: true, - }) - .then((content) => setContent(content)); + safeCall(() => + EditorAPI.initLinkPreviewPlugin( + Components.utils.cloneInto( + { + setPreviewContent: ( + link: string, + setContent: (content: string) => void, + ) => { + const note = addon.api.convert.link2note(link); + if (!note) { + setContent( + `

Invalid note link: ${link}

`, + ); + return; + } + addon.api.convert + .link2html(link, { + noteItem: note, + dryRun: true, + usePosition: true, + }) + .then((content) => setContent(content)); + }, + openURL: (url: string) => { + Zotero.getActiveZoteroPane().loadURI(url); + }, + requireCtrl: previewType === "ctrl", }, - openURL: (url: string) => { - Zotero.getActiveZoteroPane().loadURI(url); - }, - requireCtrl: previewType === "ctrl", - }, - editor._iframeWindow, - { wrapReflectors: true, cloneFunctions: true }, + editor._iframeWindow, + { wrapReflectors: true, cloneFunctions: true }, + ), ), ); + safeCall(() => EditorAPI.initPasteMarkdownPlugin()); +} + +function safeCall(callback: () => void) { + try { + callback(); + } catch (e) { + ztoolkit.log(e as Error); + } }