Skip to content

Commit

Permalink
Merge pull request #34 from MikhaD/master
Browse files Browse the repository at this point in the history
Adding support for live preview
  • Loading branch information
denolehov committed Jan 26, 2022
2 parents c2404d0 + 6aec36f commit b37a70a
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 121 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
4 changes: 2 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
"description": "Paste URL \"into\" selected text.",
"isDesktopOnly": false,
"js": "main.js",
"version": "1.6.0"
}
"version": "1.7.0"
}
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand All @@ -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",
Expand All @@ -33,4 +33,4 @@
"path": "./node_modules/cz-conventional-changelog"
}
}
}
}
205 changes: 107 additions & 98 deletions src/core.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,28 @@
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;
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) {
Expand All @@ -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, settings);
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:
Expand All @@ -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})`;
Expand Down Expand Up @@ -181,32 +162,60 @@ function getCbText(cb: string | ClipboardEvent): string | null {
return clipboardText.trim();
}

function getWordBoundaries(editor: CodeMirror.Editor): WordBoundaries {
function getWordBoundaries(editor: Editor, settings: PluginSettings): 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 };
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;
}

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);
}
26 changes: 8 additions & 18 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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() {
Expand All @@ -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");
}
}

0 comments on commit b37a70a

Please sign in to comment.