From 5558355119fcd4a2c1026306906e92bafbc41219 Mon Sep 17 00:00:00 2001 From: Mikha Davids <31388146+MikhaD@users.noreply.github.com> Date: Sat, 1 Jan 2022 21:04:51 +0200 Subject: [PATCH 1/3] got core functionality working --- package.json | 4 +- src/core.ts | 204 +++++++++++++++++++++++++------------------------ src/main.ts | 26 ++----- src/setting.ts | 4 + 4 files changed, 119 insertions(+), 119 deletions(-) diff --git a/package.json b/package.json index e4667e9..e593756 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "commitizen": "^4.2.3", "cz-conventional-changelog": "^3.3.0", "file-url": "^4.0.0", - "obsidian": "https://github.com/obsidianmd/obsidian-api/tarball/master", + "obsidian": "0.13.11", "prettier": "2.1.2", "rollup": "^2.32.1", "rollup-plugin-node-polyfills": "^0.2.1", @@ -33,4 +33,4 @@ "path": "./node_modules/cz-conventional-changelog" } } -} +} \ No newline at end of file diff --git a/src/core.ts b/src/core.ts index f514c6e..bf43eff 100644 --- a/src/core.ts +++ b/src/core.ts @@ -1,11 +1,7 @@ import assertNever from "assert-never"; import { NothingSelected, PluginSettings } from "setting"; import fileUrl from "file-url"; - -interface WordBoundaries { - start: { line: number; ch: number }; - end: { line: number; ch: number }; -} +import { Editor, EditorPosition, EditorRange } from "obsidian"; // https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch08s18.html const win32Path = /^[a-z]:\\(?:[^\\/:*?"<>|\r\n]+\\)*[^\\/:*?"<>|\r\n]*$/i; @@ -13,35 +9,20 @@ const unixPath = /^(?:\/[^/]+)+\/?$/i; const testFilePath = (url: string) => win32Path.test(url) || unixPath.test(url); /** - * @param cm CodeMirror Instance + * @param editor Obsidian Editor Instance * @param cbString text on clipboard * @param settings plugin settings */ -export default function UrlIntoSelection( - cm: CodeMirror.Editor, - cbString: string, - settings: PluginSettings -): void; +export default function UrlIntoSelection(editor: Editor, cbString: string, settings: PluginSettings): void; /** - * @param cm CodeMirror Instance + * @param editor Obsidian Editor Instance * @param cbEvent clipboard event * @param settings plugin settings */ -export default function UrlIntoSelection( - cm: CodeMirror.Editor, - cbEvent: ClipboardEvent, - settings: PluginSettings -): void; -export default function UrlIntoSelection( - cm: CodeMirror.Editor, - cb: string | ClipboardEvent, - settings: PluginSettings -): void { +export default function UrlIntoSelection(editor: Editor, cbEvent: ClipboardEvent, settings: PluginSettings): void; +export default function UrlIntoSelection(editor: Editor, cb: string | ClipboardEvent, settings: PluginSettings): void { // skip all if nothing should be done - if ( - !cm.somethingSelected() && - settings.nothingSelected === NothingSelected.doNothing - ) + if (!editor.somethingSelected() && settings.nothingSelected === NothingSelected.doNothing) return; if (typeof cb !== "string" && cb.clipboardData === null) { @@ -52,41 +33,36 @@ export default function UrlIntoSelection( const clipboardText = getCbText(cb); if (clipboardText === null) return; - const { selectedText, replaceRange } = getSelnRange(cm, settings); + const { selectedText, replaceRange } = getSelnRange(editor, settings); const replaceText = getReplaceText(clipboardText, selectedText, settings); if (replaceText === null) return; // apply changes - if (typeof cb !== "string") cb.preventDefault(); // disable default copy behavior - replace(cm, replaceText, replaceRange); - - if ( - !cm.somethingSelected() && - settings.nothingSelected === NothingSelected.insertInline - ) { - cm.setCursor({ - ch: replaceRange.start.ch + 1, - line: replaceRange.start.line, - }); + if (typeof cb !== "string") cb.preventDefault(); // prevent default paste behavior + replace(editor, replaceText, replaceRange); + + // if nothing is selected and the nothing selected behavior is to insert [](url) place the cursor between the square brackets + if ((selectedText === "") && settings.nothingSelected === NothingSelected.insertInline) { + editor.setCursor({ ch: replaceRange.from.ch + 1, line: replaceRange.from.line }); } } -function getSelnRange(cm: CodeMirror.Editor, settings: PluginSettings) { +function getSelnRange(editor: Editor, settings: PluginSettings) { let selectedText: string; - let replaceRange: WordBoundaries | null; + let replaceRange: EditorRange | null; - if (cm.somethingSelected()) { - selectedText = cm.getSelection().trim(); + if (editor.somethingSelected()) { + selectedText = editor.getSelection().trim(); replaceRange = null; } else { switch (settings.nothingSelected) { case NothingSelected.autoSelect: - replaceRange = getWordBoundaries(cm); - selectedText = cm.getRange(replaceRange.start, replaceRange.end); + replaceRange = getWordBoundaries(editor); + selectedText = editor.getRange(replaceRange.from, replaceRange.to); break; case NothingSelected.insertInline: case NothingSelected.insertBare: - replaceRange = getCursor(cm); + replaceRange = getCursor(editor); selectedText = ""; break; case NothingSelected.doNothing: @@ -98,52 +74,57 @@ function getSelnRange(cm: CodeMirror.Editor, settings: PluginSettings) { return { selectedText, replaceRange }; } -function getReplaceText( - clipboardText: string, - selectedText: string, - settings: PluginSettings -): string | null { - const isUrl = (text: string): boolean => { - if (text === "") return false; - try { - // throw TypeError: Invalid URL if not valid - new URL(text); - return true; - } catch (error) { - // settings.regex: fallback test allows url without protocol (http,file...) - return testFilePath(text) || new RegExp(settings.regex).test(text); - } - }; - const isImgEmbed = (text: string): boolean => { - const rules = settings.listForImgEmbed - .split("\n") - .filter((v) => v.length > 0) - .map((v) => new RegExp(v)); - for (const reg of rules) { - if (reg.test(text)) return true; - } - return false; - }; +function isUrl(text: string, settings: PluginSettings): boolean { + if (text === "") return false; + try { + // throw TypeError: Invalid URL if not valid + new URL(text); + return true; + } catch (error) { + // settings.regex: fallback test allows url without protocol (http,file...) + return testFilePath(text) || new RegExp(settings.regex).test(text); + } +} + +function isImgEmbed(text: string, settings: PluginSettings): boolean { + const rules = settings.listForImgEmbed + .split("\n") + .filter((v) => v.length > 0) + .map((v) => new RegExp(v)); + for (const reg of rules) { + if (reg.test(text)) return true; + } + return false; +} + +/** + * Validate that either the text on the clipboard or the selected text is a link, and if so return the link as + * a markdown link with the selected text as the link's text, or, if the value on the clipboard is not a link + * but the selected text is, the value of the clipboard as the link's text. + * If the link matches one of the image url regular expressions return a markdown image link. + * @param clipboardText text on the clipboard. + * @param selectedText highlighted text + * @param settings plugin settings + * @returns a mardown link or image link if the clipboard or selction value is a valid link, else null. + */ +function getReplaceText(clipboardText: string, selectedText: string, settings: PluginSettings): string | null { let linktext: string; let url: string; - if (isUrl(clipboardText)) { + if (isUrl(clipboardText, settings)) { linktext = selectedText; url = clipboardText; - } else if (isUrl(selectedText)) { + } else if (isUrl(selectedText, settings)) { linktext = clipboardText; url = selectedText; } else return null; // if neither of two is an URL, the following code would be skipped. - const imgEmbedMark = isImgEmbed(clipboardText) ? "!" : ""; + const imgEmbedMark = isImgEmbed(clipboardText, settings) ? "!" : ""; url = processUrl(url); - if ( - selectedText === "" && - settings.nothingSelected === NothingSelected.insertBare - ) { + if (selectedText === "" && settings.nothingSelected === NothingSelected.insertBare) { return `<${url}>`; } else { return imgEmbedMark + `[${linktext}](${url})`; @@ -181,32 +162,57 @@ function getCbText(cb: string | ClipboardEvent): string | null { return clipboardText.trim(); } -function getWordBoundaries(editor: CodeMirror.Editor): WordBoundaries { +function getWordBoundaries(editor: Editor): EditorRange { const cursor = editor.getCursor(); - - let wordBoundaries: WordBoundaries; - if (editor.getTokenTypeAt(cursor) === "url") { - const { start: startCh, end: endCh } = editor.getTokenAt(cursor); - const line = cursor.line; - wordBoundaries = { start: { line, ch: startCh }, end: { line, ch: endCh } }; - } else { - const { anchor: start, head: end } = editor.findWordAt(cursor); - wordBoundaries = { start, end }; - } + let wordBoundaries: EditorRange; + + // if (editor.getTokenTypeAt(cursor) === "url") { + // const { start: startCh, end: endCh } = editor.getTokenAt(cursor); + // const line = cursor.line; + // wordBoundaries = { from: { line, ch: startCh }, to: { line, ch: endCh } }; + // } else + wordBoundaries = findWordAt(editor.getLine(cursor.ch), cursor);; return wordBoundaries; } -function getCursor(editor: CodeMirror.Editor): WordBoundaries { - return { start: editor.getCursor(), end: editor.getCursor() }; +const findWordAt = (() => { + const nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; + + function isWordChar(char: string) { + return /\w/.test(char) || char > "\x80" && + (char.toUpperCase() != char.toLowerCase() || nonASCIISingleCaseWordChar.test(char)); + } + + return (line: string, pos: EditorPosition): EditorRange => { + let check; + let start = pos.ch; + let end = pos.ch; + (end === line.length) ? --start : ++end; + const startChar = line.charAt(pos.ch); + + if (isWordChar(startChar)) { + check = (ch: string) => isWordChar(ch); + } else if (/\s/.test(startChar)) { + check = (ch: string) => /\s/.test(ch); + } else { + check = (ch: string) => (!/\s/.test(ch) && !isWordChar(ch)); + } + + while (start > 0 && check(line.charAt(start - 1))) --start; + while (end < line.length && check(line.charAt(end))) ++end; + return { from: { line: pos.line, ch: start }, to: { line: pos.line, ch: end } }; + }; +})(); + +function getCursor(editor: Editor): EditorRange { + return { from: editor.getCursor(), to: editor.getCursor() }; } -function replace( - cm: CodeMirror.Editor, - replaceText: string, - replaceRange: WordBoundaries | null = null -): void { - if (replaceRange && replaceRange.start && replaceRange.end) - cm.replaceRange(replaceText, replaceRange.start, replaceRange.end); +function replace(editor: Editor, replaceText: string, replaceRange: EditorRange | null = null): void { + // replaceRange is only not null when there isn't anything selected. + if (replaceRange && replaceRange.from && replaceRange.to) { + editor.replaceRange(replaceText, replaceRange.from, replaceRange.to); + } // if word is null or undefined - else cm.replaceSelection(replaceText); + else editor.replaceSelection(replaceText); } diff --git a/src/main.ts b/src/main.ts index 63c4986..391346f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,4 @@ -import { MarkdownView, Plugin } from "obsidian"; -import * as CodeMirror from "codemirror"; +import { Editor, MarkdownView, Plugin } from "obsidian"; import UrlIntoSelection from "./core"; import { PluginSettings, @@ -10,33 +9,31 @@ import { export default class UrlIntoSel_Plugin extends Plugin { settings: PluginSettings; - pasteHandler = (cm: CodeMirror.Editor, e: ClipboardEvent) => - UrlIntoSelection(cm, e, this.settings); + // pasteHandler = (cm: CodeMirror.Editor, e: ClipboardEvent) => UrlIntoSelection(cm, e, this.settings); + pasteHandler = (evt: ClipboardEvent, editor: Editor) => UrlIntoSelection(editor, evt, this.settings); + async onload() { console.log("loading url-into-selection"); - await this.loadSettings(); + this.addSettingTab(new UrlIntoSelectionSettingsTab(this.app, this)); this.addCommand({ id: "paste-url-into-selection", name: "", - callback: async () => { - const editor = this.getEditor(); + editorCallback: async (editor: Editor) => { const clipboardText = await navigator.clipboard.readText(); UrlIntoSelection(editor, clipboardText, this.settings); }, }); - this.registerCodeMirror((cm: CodeMirror.Editor) => { - cm.on("paste", this.pasteHandler); - }); + this.app.workspace.on("editor-paste", this.pasteHandler); } onunload() { console.log("unloading url-into-selection"); - this.registerCodeMirror((cm) => cm.off("paste", this.pasteHandler)); + this.app.workspace.off("editor-paste", this.pasteHandler); } async loadSettings() { @@ -46,11 +43,4 @@ export default class UrlIntoSel_Plugin extends Plugin { async saveSettings() { await this.saveData(this.settings); } - - private getEditor(): CodeMirror.Editor { - let activeLeaf = this.app.workspace.activeLeaf; - if (activeLeaf.view instanceof MarkdownView) { - return activeLeaf.view.sourceMode.cmEditor; - } else throw new Error("activeLeaf.view not MarkdownView"); - } } diff --git a/src/setting.ts b/src/setting.ts index f07a7f2..551ef91 100644 --- a/src/setting.ts +++ b/src/setting.ts @@ -2,9 +2,13 @@ import UrlIntoSel_Plugin from "main"; import { PluginSettingTab, Setting } from "obsidian"; export const enum NothingSelected { + /** Default paste behaviour */ doNothing, + /** Automatically select word surrounding the cursor */ autoSelect, + /** Insert `[](url)` */ insertInline, + /** Insert `` */ insertBare, } From ff2c37bf0c12a1f7fa4976d070d71bb448c4d8bb Mon Sep 17 00:00:00 2001 From: Mikha Davids <31388146+MikhaD@users.noreply.github.com> Date: Tue, 4 Jan 2022 21:40:17 +0200 Subject: [PATCH 2/3] Implemented auto select mode --- src/core.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/core.ts b/src/core.ts index bf43eff..b4cc1a8 100644 --- a/src/core.ts +++ b/src/core.ts @@ -57,7 +57,7 @@ function getSelnRange(editor: Editor, settings: PluginSettings) { } else { switch (settings.nothingSelected) { case NothingSelected.autoSelect: - replaceRange = getWordBoundaries(editor); + replaceRange = getWordBoundaries(editor, settings); selectedText = editor.getRange(replaceRange.from, replaceRange.to); break; case NothingSelected.insertInline: @@ -162,16 +162,20 @@ function getCbText(cb: string | ClipboardEvent): string | null { return clipboardText.trim(); } -function getWordBoundaries(editor: Editor): EditorRange { +function getWordBoundaries(editor: Editor, settings: PluginSettings): EditorRange { const cursor = editor.getCursor(); - let wordBoundaries: EditorRange; - - // if (editor.getTokenTypeAt(cursor) === "url") { - // const { start: startCh, end: endCh } = editor.getTokenAt(cursor); - // const line = cursor.line; - // wordBoundaries = { from: { line, ch: startCh }, to: { line, ch: endCh } }; - // } else - wordBoundaries = findWordAt(editor.getLine(cursor.ch), cursor);; + const line = editor.getLine(cursor.line); + let wordBoundaries = findWordAt(line, cursor);; + + // If the token the cursor is on is a url, grab the whole thing instead of just parsing it like a word + let start = wordBoundaries.from.ch; + let end = wordBoundaries.to.ch; + while (start > 0 && !/\s/.test(line.charAt(start - 1))) --start; + while (end < line.length && !/\s/.test(line.charAt(end))) ++end; + if (isUrl(line.slice(start, end), settings)) { + wordBoundaries.from.ch = start; + wordBoundaries.to.ch = end; + } return wordBoundaries; } @@ -189,7 +193,6 @@ const findWordAt = (() => { let end = pos.ch; (end === line.length) ? --start : ++end; const startChar = line.charAt(pos.ch); - if (isWordChar(startChar)) { check = (ch: string) => isWordChar(ch); } else if (/\s/.test(startChar)) { From 6aec36f0b711742547061784b8458b7b4654d96f Mon Sep 17 00:00:00 2001 From: Mikha Davids <31388146+MikhaD@users.noreply.github.com> Date: Tue, 4 Jan 2022 21:55:51 +0200 Subject: [PATCH 3/3] updated package, manifest and changelog files --- CHANGELOG.md | 5 +++++ manifest.json | 4 ++-- package.json | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d7fdb7..5b4c1cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.7.0] + +### Bug Fixes +* Fixed the plugin so that it works in live preview, closes [#33](https://github.com/denolehov/obsidian-url-into-selection/issues/33) + ## [1.6.0](https://github.com/denolehov/obsidian-url-into-selection/compare/v1.1.0...v1.6.0) (2021-04-27) diff --git a/manifest.json b/manifest.json index 0ca890c..e90f507 100644 --- a/manifest.json +++ b/manifest.json @@ -4,5 +4,5 @@ "description": "Paste URL \"into\" selected text.", "isDesktopOnly": false, "js": "main.js", - "version": "1.6.0" -} + "version": "1.7.0" +} \ No newline at end of file diff --git a/package.json b/package.json index e593756..3a04967 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-url-into-selection", - "version": "1.6.0", + "version": "1.7.0", "description": "Paste URL \"into\" selected text", "main": "main.js", "scripts": {