Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for live preview #34

Merged
merged 3 commits into from
Jan 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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");
}
}
Loading