Skip to content

Commit b37a70a

Browse files
authoredJan 26, 2022
Merge pull request #34 from MikhaD/master
Adding support for live preview
2 parents c2404d0 + 6aec36f commit b37a70a

File tree

6 files changed

+129
-121
lines changed

6 files changed

+129
-121
lines changed
 

‎CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
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.
44

5+
## [1.7.0]
6+
7+
### Bug Fixes
8+
* Fixed the plugin so that it works in live preview, closes [#33](https://github.com/denolehov/obsidian-url-into-selection/issues/33)
9+
510
## [1.6.0](https://github.com/denolehov/obsidian-url-into-selection/compare/v1.1.0...v1.6.0) (2021-04-27)
611

712

‎manifest.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
"description": "Paste URL \"into\" selected text.",
55
"isDesktopOnly": false,
66
"js": "main.js",
7-
"version": "1.6.0"
8-
}
7+
"version": "1.7.0"
8+
}

‎package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "obsidian-url-into-selection",
3-
"version": "1.6.0",
3+
"version": "1.7.0",
44
"description": "Paste URL \"into\" selected text",
55
"main": "main.js",
66
"scripts": {
@@ -19,7 +19,7 @@
1919
"commitizen": "^4.2.3",
2020
"cz-conventional-changelog": "^3.3.0",
2121
"file-url": "^4.0.0",
22-
"obsidian": "https://github.com/obsidianmd/obsidian-api/tarball/master",
22+
"obsidian": "0.13.11",
2323
"prettier": "2.1.2",
2424
"rollup": "^2.32.1",
2525
"rollup-plugin-node-polyfills": "^0.2.1",
@@ -33,4 +33,4 @@
3333
"path": "./node_modules/cz-conventional-changelog"
3434
}
3535
}
36-
}
36+
}

‎src/core.ts

+107-98
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,28 @@
11
import assertNever from "assert-never";
22
import { NothingSelected, PluginSettings } from "setting";
33
import fileUrl from "file-url";
4-
5-
interface WordBoundaries {
6-
start: { line: number; ch: number };
7-
end: { line: number; ch: number };
8-
}
4+
import { Editor, EditorPosition, EditorRange } from "obsidian";
95

106
// https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch08s18.html
117
const win32Path = /^[a-z]:\\(?:[^\\/:*?"<>|\r\n]+\\)*[^\\/:*?"<>|\r\n]*$/i;
128
const unixPath = /^(?:\/[^/]+)+\/?$/i;
139
const testFilePath = (url: string) => win32Path.test(url) || unixPath.test(url);
1410

1511
/**
16-
* @param cm CodeMirror Instance
12+
* @param editor Obsidian Editor Instance
1713
* @param cbString text on clipboard
1814
* @param settings plugin settings
1915
*/
20-
export default function UrlIntoSelection(
21-
cm: CodeMirror.Editor,
22-
cbString: string,
23-
settings: PluginSettings
24-
): void;
16+
export default function UrlIntoSelection(editor: Editor, cbString: string, settings: PluginSettings): void;
2517
/**
26-
* @param cm CodeMirror Instance
18+
* @param editor Obsidian Editor Instance
2719
* @param cbEvent clipboard event
2820
* @param settings plugin settings
2921
*/
30-
export default function UrlIntoSelection(
31-
cm: CodeMirror.Editor,
32-
cbEvent: ClipboardEvent,
33-
settings: PluginSettings
34-
): void;
35-
export default function UrlIntoSelection(
36-
cm: CodeMirror.Editor,
37-
cb: string | ClipboardEvent,
38-
settings: PluginSettings
39-
): void {
22+
export default function UrlIntoSelection(editor: Editor, cbEvent: ClipboardEvent, settings: PluginSettings): void;
23+
export default function UrlIntoSelection(editor: Editor, cb: string | ClipboardEvent, settings: PluginSettings): void {
4024
// skip all if nothing should be done
41-
if (
42-
!cm.somethingSelected() &&
43-
settings.nothingSelected === NothingSelected.doNothing
44-
)
25+
if (!editor.somethingSelected() && settings.nothingSelected === NothingSelected.doNothing)
4526
return;
4627

4728
if (typeof cb !== "string" && cb.clipboardData === null) {
@@ -52,41 +33,36 @@ export default function UrlIntoSelection(
5233
const clipboardText = getCbText(cb);
5334
if (clipboardText === null) return;
5435

55-
const { selectedText, replaceRange } = getSelnRange(cm, settings);
36+
const { selectedText, replaceRange } = getSelnRange(editor, settings);
5637
const replaceText = getReplaceText(clipboardText, selectedText, settings);
5738
if (replaceText === null) return;
5839

5940
// apply changes
60-
if (typeof cb !== "string") cb.preventDefault(); // disable default copy behavior
61-
replace(cm, replaceText, replaceRange);
62-
63-
if (
64-
!cm.somethingSelected() &&
65-
settings.nothingSelected === NothingSelected.insertInline
66-
) {
67-
cm.setCursor({
68-
ch: replaceRange.start.ch + 1,
69-
line: replaceRange.start.line,
70-
});
41+
if (typeof cb !== "string") cb.preventDefault(); // prevent default paste behavior
42+
replace(editor, replaceText, replaceRange);
43+
44+
// if nothing is selected and the nothing selected behavior is to insert [](url) place the cursor between the square brackets
45+
if ((selectedText === "") && settings.nothingSelected === NothingSelected.insertInline) {
46+
editor.setCursor({ ch: replaceRange.from.ch + 1, line: replaceRange.from.line });
7147
}
7248
}
7349

74-
function getSelnRange(cm: CodeMirror.Editor, settings: PluginSettings) {
50+
function getSelnRange(editor: Editor, settings: PluginSettings) {
7551
let selectedText: string;
76-
let replaceRange: WordBoundaries | null;
52+
let replaceRange: EditorRange | null;
7753

78-
if (cm.somethingSelected()) {
79-
selectedText = cm.getSelection().trim();
54+
if (editor.somethingSelected()) {
55+
selectedText = editor.getSelection().trim();
8056
replaceRange = null;
8157
} else {
8258
switch (settings.nothingSelected) {
8359
case NothingSelected.autoSelect:
84-
replaceRange = getWordBoundaries(cm);
85-
selectedText = cm.getRange(replaceRange.start, replaceRange.end);
60+
replaceRange = getWordBoundaries(editor, settings);
61+
selectedText = editor.getRange(replaceRange.from, replaceRange.to);
8662
break;
8763
case NothingSelected.insertInline:
8864
case NothingSelected.insertBare:
89-
replaceRange = getCursor(cm);
65+
replaceRange = getCursor(editor);
9066
selectedText = "";
9167
break;
9268
case NothingSelected.doNothing:
@@ -98,52 +74,57 @@ function getSelnRange(cm: CodeMirror.Editor, settings: PluginSettings) {
9874
return { selectedText, replaceRange };
9975
}
10076

101-
function getReplaceText(
102-
clipboardText: string,
103-
selectedText: string,
104-
settings: PluginSettings
105-
): string | null {
106-
const isUrl = (text: string): boolean => {
107-
if (text === "") return false;
108-
try {
109-
// throw TypeError: Invalid URL if not valid
110-
new URL(text);
111-
return true;
112-
} catch (error) {
113-
// settings.regex: fallback test allows url without protocol (http,file...)
114-
return testFilePath(text) || new RegExp(settings.regex).test(text);
115-
}
116-
};
117-
const isImgEmbed = (text: string): boolean => {
118-
const rules = settings.listForImgEmbed
119-
.split("\n")
120-
.filter((v) => v.length > 0)
121-
.map((v) => new RegExp(v));
122-
for (const reg of rules) {
123-
if (reg.test(text)) return true;
124-
}
125-
return false;
126-
};
77+
function isUrl(text: string, settings: PluginSettings): boolean {
78+
if (text === "") return false;
79+
try {
80+
// throw TypeError: Invalid URL if not valid
81+
new URL(text);
82+
return true;
83+
} catch (error) {
84+
// settings.regex: fallback test allows url without protocol (http,file...)
85+
return testFilePath(text) || new RegExp(settings.regex).test(text);
86+
}
87+
}
88+
89+
function isImgEmbed(text: string, settings: PluginSettings): boolean {
90+
const rules = settings.listForImgEmbed
91+
.split("\n")
92+
.filter((v) => v.length > 0)
93+
.map((v) => new RegExp(v));
94+
for (const reg of rules) {
95+
if (reg.test(text)) return true;
96+
}
97+
return false;
98+
}
99+
100+
/**
101+
* Validate that either the text on the clipboard or the selected text is a link, and if so return the link as
102+
* a markdown link with the selected text as the link's text, or, if the value on the clipboard is not a link
103+
* but the selected text is, the value of the clipboard as the link's text.
104+
* If the link matches one of the image url regular expressions return a markdown image link.
105+
* @param clipboardText text on the clipboard.
106+
* @param selectedText highlighted text
107+
* @param settings plugin settings
108+
* @returns a mardown link or image link if the clipboard or selction value is a valid link, else null.
109+
*/
110+
function getReplaceText(clipboardText: string, selectedText: string, settings: PluginSettings): string | null {
127111

128112
let linktext: string;
129113
let url: string;
130114

131-
if (isUrl(clipboardText)) {
115+
if (isUrl(clipboardText, settings)) {
132116
linktext = selectedText;
133117
url = clipboardText;
134-
} else if (isUrl(selectedText)) {
118+
} else if (isUrl(selectedText, settings)) {
135119
linktext = clipboardText;
136120
url = selectedText;
137121
} else return null; // if neither of two is an URL, the following code would be skipped.
138122

139-
const imgEmbedMark = isImgEmbed(clipboardText) ? "!" : "";
123+
const imgEmbedMark = isImgEmbed(clipboardText, settings) ? "!" : "";
140124

141125
url = processUrl(url);
142126

143-
if (
144-
selectedText === "" &&
145-
settings.nothingSelected === NothingSelected.insertBare
146-
) {
127+
if (selectedText === "" && settings.nothingSelected === NothingSelected.insertBare) {
147128
return `<${url}>`;
148129
} else {
149130
return imgEmbedMark + `[${linktext}](${url})`;
@@ -181,32 +162,60 @@ function getCbText(cb: string | ClipboardEvent): string | null {
181162
return clipboardText.trim();
182163
}
183164

184-
function getWordBoundaries(editor: CodeMirror.Editor): WordBoundaries {
165+
function getWordBoundaries(editor: Editor, settings: PluginSettings): EditorRange {
185166
const cursor = editor.getCursor();
186-
187-
let wordBoundaries: WordBoundaries;
188-
if (editor.getTokenTypeAt(cursor) === "url") {
189-
const { start: startCh, end: endCh } = editor.getTokenAt(cursor);
190-
const line = cursor.line;
191-
wordBoundaries = { start: { line, ch: startCh }, end: { line, ch: endCh } };
192-
} else {
193-
const { anchor: start, head: end } = editor.findWordAt(cursor);
194-
wordBoundaries = { start, end };
167+
const line = editor.getLine(cursor.line);
168+
let wordBoundaries = findWordAt(line, cursor);;
169+
170+
// If the token the cursor is on is a url, grab the whole thing instead of just parsing it like a word
171+
let start = wordBoundaries.from.ch;
172+
let end = wordBoundaries.to.ch;
173+
while (start > 0 && !/\s/.test(line.charAt(start - 1))) --start;
174+
while (end < line.length && !/\s/.test(line.charAt(end))) ++end;
175+
if (isUrl(line.slice(start, end), settings)) {
176+
wordBoundaries.from.ch = start;
177+
wordBoundaries.to.ch = end;
195178
}
196179
return wordBoundaries;
197180
}
198181

199-
function getCursor(editor: CodeMirror.Editor): WordBoundaries {
200-
return { start: editor.getCursor(), end: editor.getCursor() };
182+
const findWordAt = (() => {
183+
const nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;
184+
185+
function isWordChar(char: string) {
186+
return /\w/.test(char) || char > "\x80" &&
187+
(char.toUpperCase() != char.toLowerCase() || nonASCIISingleCaseWordChar.test(char));
188+
}
189+
190+
return (line: string, pos: EditorPosition): EditorRange => {
191+
let check;
192+
let start = pos.ch;
193+
let end = pos.ch;
194+
(end === line.length) ? --start : ++end;
195+
const startChar = line.charAt(pos.ch);
196+
if (isWordChar(startChar)) {
197+
check = (ch: string) => isWordChar(ch);
198+
} else if (/\s/.test(startChar)) {
199+
check = (ch: string) => /\s/.test(ch);
200+
} else {
201+
check = (ch: string) => (!/\s/.test(ch) && !isWordChar(ch));
202+
}
203+
204+
while (start > 0 && check(line.charAt(start - 1))) --start;
205+
while (end < line.length && check(line.charAt(end))) ++end;
206+
return { from: { line: pos.line, ch: start }, to: { line: pos.line, ch: end } };
207+
};
208+
})();
209+
210+
function getCursor(editor: Editor): EditorRange {
211+
return { from: editor.getCursor(), to: editor.getCursor() };
201212
}
202213

203-
function replace(
204-
cm: CodeMirror.Editor,
205-
replaceText: string,
206-
replaceRange: WordBoundaries | null = null
207-
): void {
208-
if (replaceRange && replaceRange.start && replaceRange.end)
209-
cm.replaceRange(replaceText, replaceRange.start, replaceRange.end);
214+
function replace(editor: Editor, replaceText: string, replaceRange: EditorRange | null = null): void {
215+
// replaceRange is only not null when there isn't anything selected.
216+
if (replaceRange && replaceRange.from && replaceRange.to) {
217+
editor.replaceRange(replaceText, replaceRange.from, replaceRange.to);
218+
}
210219
// if word is null or undefined
211-
else cm.replaceSelection(replaceText);
220+
else editor.replaceSelection(replaceText);
212221
}

‎src/main.ts

+8-18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { MarkdownView, Plugin } from "obsidian";
2-
import * as CodeMirror from "codemirror";
1+
import { Editor, MarkdownView, Plugin } from "obsidian";
32
import UrlIntoSelection from "./core";
43
import {
54
PluginSettings,
@@ -10,33 +9,31 @@ import {
109
export default class UrlIntoSel_Plugin extends Plugin {
1110
settings: PluginSettings;
1211

13-
pasteHandler = (cm: CodeMirror.Editor, e: ClipboardEvent) =>
14-
UrlIntoSelection(cm, e, this.settings);
12+
// pasteHandler = (cm: CodeMirror.Editor, e: ClipboardEvent) => UrlIntoSelection(cm, e, this.settings);
13+
pasteHandler = (evt: ClipboardEvent, editor: Editor) => UrlIntoSelection(editor, evt, this.settings);
14+
1515

1616
async onload() {
1717
console.log("loading url-into-selection");
18-
1918
await this.loadSettings();
19+
2020
this.addSettingTab(new UrlIntoSelectionSettingsTab(this.app, this));
2121
this.addCommand({
2222
id: "paste-url-into-selection",
2323
name: "",
24-
callback: async () => {
25-
const editor = this.getEditor();
24+
editorCallback: async (editor: Editor) => {
2625
const clipboardText = await navigator.clipboard.readText();
2726
UrlIntoSelection(editor, clipboardText, this.settings);
2827
},
2928
});
3029

31-
this.registerCodeMirror((cm: CodeMirror.Editor) => {
32-
cm.on("paste", this.pasteHandler);
33-
});
30+
this.app.workspace.on("editor-paste", this.pasteHandler);
3431
}
3532

3633
onunload() {
3734
console.log("unloading url-into-selection");
3835

39-
this.registerCodeMirror((cm) => cm.off("paste", this.pasteHandler));
36+
this.app.workspace.off("editor-paste", this.pasteHandler);
4037
}
4138

4239
async loadSettings() {
@@ -46,11 +43,4 @@ export default class UrlIntoSel_Plugin extends Plugin {
4643
async saveSettings() {
4744
await this.saveData(this.settings);
4845
}
49-
50-
private getEditor(): CodeMirror.Editor {
51-
let activeLeaf = this.app.workspace.activeLeaf;
52-
if (activeLeaf.view instanceof MarkdownView) {
53-
return activeLeaf.view.sourceMode.cmEditor;
54-
} else throw new Error("activeLeaf.view not MarkdownView");
55-
}
5646
}

‎src/setting.ts

+4
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ import UrlIntoSel_Plugin from "main";
22
import { PluginSettingTab, Setting } from "obsidian";
33

44
export const enum NothingSelected {
5+
/** Default paste behaviour */
56
doNothing,
7+
/** Automatically select word surrounding the cursor */
68
autoSelect,
9+
/** Insert `[](url)` */
710
insertInline,
11+
/** Insert `<url>` */
812
insertBare,
913
}
1014

0 commit comments

Comments
 (0)
Please sign in to comment.