diff --git a/main.ts b/main.ts index 9b8f1df..4189938 100644 --- a/main.ts +++ b/main.ts @@ -1,15 +1,36 @@ import { CodepenEmbed } from 'embeds/codepen'; import { TwitterEmbed } from 'embeds/twitter'; import { EmbedBase } from 'embeds/embedBase'; -import { Plugin } from 'obsidian'; +import { Editor, Plugin } from 'obsidian'; import { YouTubeEmbed } from 'embeds/youtube'; import { SteamEmbed } from 'embeds/steam'; import { RedditEmbed } from 'embeds/reddit'; import { AutoEmbedSettingTab, DEFAULT_SETTINGS, PluginSettings } from 'settings-tab'; import { ImgurEmbed } from 'embeds/imgur'; import { SpotifyEmbed } from 'embeds/spotify'; +import SuggestEmbed from 'suggestEmbed'; +import { isURL, regexUrl } from 'utility'; -// Remember to rename these classes and interfaces! +class PasteInfo { + trigger: boolean; + text: string; + + constructor(trigger: boolean, text: string) { + this.trigger = trigger; + this.text = text; + } +} + +interface EditPosition { + line: number; + ch: number; +} + +interface Selection { + start: EditPosition + end: EditPosition + text: string; +} export default class AutoEmbedPlugin extends Plugin { settings: PluginSettings; @@ -21,13 +42,29 @@ export default class AutoEmbedPlugin extends Plugin { new CodepenEmbed(this), new SpotifyEmbed(this), new ImgurEmbed(this), - ] + ]; + isShiftDown = false; + pasteInfo: PasteInfo = new PasteInfo(false, ""); async onload() { console.log('loading plugin'); await this.loadSettings(); this.addSettingTab(new AutoEmbedSettingTab(this.app, this)); + + this.registerDomEvent(document, "keydown", (e) => { + if (e.shiftKey) + this.isShiftDown = true; + }) + + this.registerDomEvent(document, "keydown", (e) => { + if (!e.shiftKey) + this.isShiftDown = false; + }) + + this.registerEditorSuggest(new SuggestEmbed(this)); + + this.app.workspace.on("editor-paste", this.onPaste.bind(this)); this.registerMarkdownPostProcessor((el, ctx) => { console.log("Registering markdown") @@ -63,12 +100,37 @@ export default class AutoEmbedPlugin extends Plugin { await this.saveData(this.settings); } + private onPaste(e: ClipboardEvent, editor: Editor) { + if (e.defaultPrevented) + return; + + // Usually "Ctrl-Shift-V" means paste text without formatting. So don't embed + if (this.isShiftDown) + return; + + // Check if valid url + const clipboardData = e.clipboardData?.getData("text/plain"); + if (!clipboardData || clipboardData === "" || !isURL(clipboardData)) + return; + + this.pasteInfo.trigger = true; + this.pasteInfo.text = clipboardData; + + // TODO: Delete? + // Do this to be able to select the url. + // Obsidian pastes after this function, so + // e.preventDefault(); + // editor.replaceRange(clipboardData, editor.getCursor()); + + // this.selectLink(editor); + } + private handleAnchor(a: HTMLAnchorElement) { const innerText = a.innerText; // Removes all spaces const innerTextTrim = innerText.replace(/\s/g, ""); - if (innerTextTrim.includes("|noembed")) { + if (!innerTextTrim.includes("ae:embed")) { return; } @@ -87,9 +149,57 @@ export default class AutoEmbedPlugin extends Plugin { this.insertEmbed(a, embed); } + private getSelection(editor: Editor) : Selection | null { + if (!editor.somethingSelected()) + return null; + + return { + start: editor.getCursor("from"), + end: editor.getCursor("to"), + text: editor.getSelection() + } + } + + // Used if there's nothing selected. Mostly when the user pastes a link + selectLink(editor: Editor) { + const cursor = editor.getCursor(); + const lineText = editor.getLine(cursor.line); + console.log("line text: "+ lineText) + const matchedLinks = lineText.matchAll(regexUrl); + for (const match of matchedLinks) { + console.log("match: " + match[0]) + // Check if the cursor is within the match + console.log("start: " + match.index + "|end: " + (match.index??0 + match[0].length) + "|cursor: "+ cursor.ch); + if (match.index && + match.index <= cursor.ch && // Is start of match before cursor + match.index + match[0].length >= cursor.ch // Is end of match after cursor + ) { + console.log("Selected: " + match[0]); + editor.setSelection( + { + line: cursor.line, + ch: match.index + }, + { + line: cursor.line, + ch: match.index + match[0].length + } + ); + break; + } + } + } + + markToEmbed(selection: Selection, editor: Editor) { + // const selection = this.getSelection(editor); + editor.replaceRange(`[ae:embed](${selection.text})`, selection.start, selection.end); + + console.log(`Replacing: [ae:embed](${selection}), Start:${selection.start.ch}, End: ${selection.end.ch}`) + } + private createEmbed(embedSource: EmbedBase, link: string) { const embed = embedSource.createEmbed(link); - return embed; + return embed; } private insertEmbed(a: HTMLAnchorElement, container: HTMLElement) { @@ -97,3 +207,4 @@ export default class AutoEmbedPlugin extends Plugin { parent?.replaceChild(container, a); } } + diff --git a/suggestEmbed.ts b/suggestEmbed.ts new file mode 100644 index 0000000..a0ae966 --- /dev/null +++ b/suggestEmbed.ts @@ -0,0 +1,69 @@ +import AutoEmbedPlugin from "main"; +import { Editor, EditorPosition, EditorSuggest, EditorSuggestContext, EditorSuggestTriggerInfo, TFile } from "obsidian"; + +class Suggestion { + choice: string; + + constructor(choice: string) { + this.choice = choice; + } +} + +export default class SuggestEmbed extends EditorSuggest { + private plugin: AutoEmbedPlugin; + private editor: Editor; + + constructor(plugin: AutoEmbedPlugin) { + super(plugin.app); + this.plugin = plugin; + } + + onTrigger(cursor: EditorPosition, editor: Editor, file: TFile | null): EditorSuggestTriggerInfo | null { + console.log("on trigger"); + this.editor = editor; + if (!this.plugin.pasteInfo.trigger) + return null; + + this.plugin.pasteInfo.trigger = false; + console.log("Cursor-ch: " + cursor.ch); + + return { + start: cursor, + end: cursor, + query: this.plugin.pasteInfo.text, + } + } + + getSuggestions(context: EditorSuggestContext): Suggestion[] | Promise { + console.log("get suggestion"); + return [new Suggestion("Embed Link"), new Suggestion("Cancel")]; + } + + renderSuggestion(suggestion: Suggestion, el: HTMLElement): void { + console.log("render suggestion"); + el.setText(suggestion.choice); + } + + selectSuggestion(suggestion: Suggestion, e: KeyboardEvent | MouseEvent): void { + console.log("select suggestion"); + const cursor = this.editor.getCursor(); + if (suggestion.choice === "Embed Link") { + console.log("Selected create embed"); + const text = this.plugin.pasteInfo.text; + this.plugin.markToEmbed( + { + text: text, + start: { + line: cursor.line, + ch: cursor.ch - text.length + }, + end: cursor + } + ,this.editor); + } + else + console.log("select suggestion: " + suggestion.choice); + + this.close(); + } +} \ No newline at end of file diff --git a/utility.ts b/utility.ts new file mode 100644 index 0000000..d65234b --- /dev/null +++ b/utility.ts @@ -0,0 +1,10 @@ +export const regexUrl = /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gi + +export function isURL(str: string) : boolean { + try { + new URL(str); + return true; + } catch { + return false; + } +} \ No newline at end of file