From 098e0dbdc331f038bb39952fde392c90b3d970bb Mon Sep 17 00:00:00 2001 From: GnoxNahte Date: Wed, 23 Oct 2024 22:51:23 +0800 Subject: [PATCH] Try a different method of finding the correct embed to use given the url Brief explanation of method: Get the hostname -> Find embed using dictionary, with the hostname as key Tested performance and didn't see any noticeable changes. In fact, the old method (using regex for each website), took < 0.3s even when CPU was slowed down 6x. Should've tested before doing this... --- src/embed-state-field.ts | 8 +++-- src/embeds/codepen.ts | 1 + src/embeds/embedBase.ts | 4 ++- src/embeds/embedManager.ts | 69 ++++++++++++++++++++++++++++---------- src/embeds/googleDocs.ts | 1 + src/embeds/imgur.ts | 1 + src/embeds/instagram.ts | 1 + src/embeds/reddit.ts | 3 +- src/embeds/soundcloud.ts | 1 + src/embeds/spotify.ts | 1 + src/embeds/steam.ts | 1 + src/embeds/tiktok.ts | 1 + src/embeds/twitter.ts | 1 + src/embeds/youtube.ts | 1 + src/main.ts | 13 +++---- src/preview-embed-modal.ts | 8 ++++- src/utility.ts | 11 +++--- 17 files changed, 92 insertions(+), 34 deletions(-) diff --git a/src/embed-state-field.ts b/src/embed-state-field.ts index a54192a..f633dcf 100644 --- a/src/embed-state-field.ts +++ b/src/embed-state-field.ts @@ -4,7 +4,7 @@ import { Decoration, DecorationSet, EditorView } from "@codemirror/view" import { EmbedWidget } from "./embed-widget"; import { EmbedManager } from "./embeds/embedManager"; import { editorLivePreviewField } from "obsidian"; -import { isLinkToImage, isURL } from "./utility"; +import { isLinkToImage, getUrlHostname } from "./utility"; const formattingImageMarkerRegex = /formatting_formatting-image_image_image-marker(?:_list-\d*)?$/; const stringUrlRegex = /^(?:list-\d*_)?string_url$/; @@ -44,10 +44,12 @@ export const embedField = StateField.define({ altTextStartPos = null; // Reset it - if (!isURL(url) || isLinkToImage(url)) + const hostName = getUrlHostname(url); + + if (hostName === null || isLinkToImage(url)) return; - const embedData = EmbedManager.getEmbedData(url, alt); + const embedData = EmbedManager.getEmbedData(hostName, url, alt); if (embedData === null) return; diff --git a/src/embeds/codepen.ts b/src/embeds/codepen.ts index f215556..cafbfc5 100644 --- a/src/embeds/codepen.ts +++ b/src/embeds/codepen.ts @@ -3,6 +3,7 @@ import { EmbedBase } from "./embedBase"; export class CodepenEmbed extends EmbedBase { name: SupportedWebsites = "CodePen"; + hostnames = ["codepen.io", "codepen.com"]; regex = new RegExp(/https:\/\/codepen\.io\/([\w-]+)\/pen\/(\w+)(\?.*)?/); createEmbed(url: string): HTMLElement { diff --git a/src/embeds/embedBase.ts b/src/embeds/embedBase.ts index 5ec4842..7545bee 100644 --- a/src/embeds/embedBase.ts +++ b/src/embeds/embedBase.ts @@ -34,6 +34,8 @@ export interface EmbedResult { export abstract class EmbedBase { readonly autoEmbedCssClass: string = "auto-embed"; readonly name: SupportedWebsites | "Other" | "Fallback"; + // To quickly identify if the link is for this embed + readonly hostnames: string[]; // To identify if the anchor link matches the embed type readonly regex: RegExp; // Embed websites usually post a resize message. @@ -263,7 +265,7 @@ export abstract class EmbedBase { const error = createEl("p", {cls: `${this.autoEmbedCssClass} error-embed`}); error.setText(errorMsg); - console.log("auto-embed/error: " + errorMsg); + console.error("auto-embed/error: " + errorMsg); return error; } } \ No newline at end of file diff --git a/src/embeds/embedManager.ts b/src/embeds/embedManager.ts index 0145d8c..d30b237 100644 --- a/src/embeds/embedManager.ts +++ b/src/embeds/embedManager.ts @@ -28,8 +28,9 @@ export class EmbedManager { plugin: AutoEmbedPlugin; embedSources: EmbedBase[]; - ignoredDomains: RegExp[]; + ignoredDomains: string[]; defaultFallbackEmbed: DefaultFallbackEmbed; + hostNameDictionary: Record; init(plugin: AutoEmbedPlugin) { this.plugin = plugin; @@ -47,13 +48,27 @@ export class EmbedManager { new InstagramEmbed(plugin), ]; + // Build the dictionary + this.hostNameDictionary = { }; + this.embedSources.forEach((source) => { + source.hostnames.forEach((hostName) => { + this.hostNameDictionary[hostName] = source; + }); + }); + // Having some trouble replacing the embedded web pages from Obsidian. // So remove YouTube and Twitter (Keep "TwitterEmbed" for x.com though, since Obsidian doesn't embed those) this.ignoredDomains = [ - // Ignore embeds for youtube and twtiter - new RegExp(/(?:https?:\/\/)?(?:www\.)?youtu(?:\.be\/|be.com\/)/) - ]; + // Ignore embeds for youtube + "youtube.com", + "m.youtube.com", + "youtu.be", + "youtube-nocookie.com", + // Ignore embeds for twitter.com + // In the next step, check the Obsidian version and decide if ignore x.com too + "" + ]; // Obsidian starts supporting x.com embeds from version 1.7.0 onwards. // So, check which version the user is currently on, then @@ -67,13 +82,11 @@ export class EmbedManager { if (majorVersion > 1 || (majorVersion === 1 && minorVersion >= 7)) { - // Ignore twitter & X - this.ignoredDomains.push(new RegExp(/https:\/\/(?:twitter|x)\.com/)) - } + // Ignore twitter & X + this.ignoredDomains.push("x.com"); + } else { this.embedSources.push(new TwitterEmbed(plugin)); - // Ignore twitter only - this.ignoredDomains.push(new RegExp(/https:\/\/(?:twitter)\.com/)); } this.defaultFallbackEmbed = new DefaultFallbackEmbed(plugin); @@ -81,18 +94,33 @@ export class EmbedManager { // Gets the embed source for the url // Returns null if it can't / shouldn't be embedded. - static getEmbedData(url: string, alt: string): BaseEmbedData | null{ - const domain = this._instance.ignoredDomains.find(domain => { - return domain.test(url); - }); + static getEmbedData(hostname: string, url: string, alt: string): BaseEmbedData | null{ + performance.mark("get-embed-start"); // If found a domain in the ignored domains, return - if (domain) { + if (this._instance.ignoredDomains.contains(hostname)) return null; + + let embedSource: EmbedBase | null = null; + + // First try of finding embedSource + embedSource = this._instance.hostNameDictionary[hostname]; + + // Second try of finding embedSource, + // Reduce the subdomains one level at a time. + if (!embedSource) { + const domainParts = hostname.split(".") + for (let i = 1; i < domainParts.length - 1; i++) { + const domain = domainParts.slice(i).join('.'); + const source = this._instance.hostNameDictionary[domain]; + if (source) { + embedSource = source; + break; + } + } } - const embedSource = this._instance.embedSources.find((source) => { - return source.regex.test(url); - }) ?? this._instance.defaultFallbackEmbed; + if (!embedSource || !embedSource.regex.test(url)) + embedSource = this._instance.defaultFallbackEmbed; // TODO: Consider moving this up. If it's at the start, need to get the top level domain then filter the websites. // Skips any regex and other checks too @@ -100,6 +128,13 @@ export class EmbedManager { if (embedSource !== this._instance.defaultFallbackEmbed && isWebsiteEnabled) { return null; } + performance.mark("get-embed-end"); + + const measure = performance.measure("get-embed-measure", "get-embed-start", "get-embed-end"); + console.log(embedSource.name + " : " + measure.duration); + performance.clearMarks(); + performance.clearMeasures(); + performance.clearResourceTimings(); const options = embedSource.getOptions(alt); diff --git a/src/embeds/googleDocs.ts b/src/embeds/googleDocs.ts index a159c5e..3ba8755 100644 --- a/src/embeds/googleDocs.ts +++ b/src/embeds/googleDocs.ts @@ -3,6 +3,7 @@ import { EmbedBase } from "./embedBase"; export class GoogleDocsEmbed extends EmbedBase { name: SupportedWebsites = "Google Docs"; + hostnames = ["docs.google.com"]; regex = new RegExp(/https:\/\/docs\.google\.com\/document\/d\/(\w+)/); createEmbed(url: string): HTMLElement { diff --git a/src/embeds/imgur.ts b/src/embeds/imgur.ts index b40d8d8..fecfe55 100644 --- a/src/embeds/imgur.ts +++ b/src/embeds/imgur.ts @@ -3,6 +3,7 @@ import { EmbedBase } from "./embedBase"; export class ImgurEmbed extends EmbedBase { name: SupportedWebsites = "Imgur"; + hostnames = ["imgur.com"]; regex = new RegExp(/https:\/\/imgur\.com\/(?:(?:a|gallery|(?:t\/\w+))\/)?(\w+)/); embedOrigin = "https://imgur.com"; diff --git a/src/embeds/instagram.ts b/src/embeds/instagram.ts index 8344a49..fbd10c4 100644 --- a/src/embeds/instagram.ts +++ b/src/embeds/instagram.ts @@ -3,6 +3,7 @@ import { EmbedBase } from "./embedBase"; export class InstagramEmbed extends EmbedBase { name: SupportedWebsites = "Instagram"; + hostnames = ["instagram.com"]; regex = new RegExp(/https:\/\/(?:www\.)?instagram\.com\/(?:(?:(?:[\w._(?:[\w._]+\/)?(?:p|reel)\/([\w\-_]+))|(?:[\w._(?:[\w._]+))/); createEmbed(url: string): HTMLElement { diff --git a/src/embeds/reddit.ts b/src/embeds/reddit.ts index e8f84f4..f6ac4fc 100644 --- a/src/embeds/reddit.ts +++ b/src/embeds/reddit.ts @@ -4,13 +4,14 @@ import { EmbedBase } from "./embedBase"; export class RedditEmbed extends EmbedBase { name: SupportedWebsites = "Reddit"; regex = new RegExp(/reddit.com/); + hostnames = ["reddit.com"]; embedOrigin = "https://embed.reddit.com"; createEmbed(url: string): HTMLElement { const regexMatch = url.match(this.regex); // Shouldn't happen since got test before. But in case if (regexMatch === null) - return this.onErrorCreatingEmbed(url); + return this.onErrorCreatingEmbed(url); const postIdRegexResult = url.match(/\/(?:comments|s)\/(\w+)/) as RegExpMatchArray; if (!postIdRegexResult) diff --git a/src/embeds/soundcloud.ts b/src/embeds/soundcloud.ts index 5965727..d0e71ac 100644 --- a/src/embeds/soundcloud.ts +++ b/src/embeds/soundcloud.ts @@ -3,6 +3,7 @@ import { EmbedBase } from "./embedBase"; export class SoundCloudEmbed extends EmbedBase { name: SupportedWebsites = "SoundCloud"; + hostnames = ["soundcloud.com"]; regex = new RegExp(/https:\/\/soundcloud\.com\/(.*)/); createEmbed(url: string): HTMLElement { const regexMatch = url.match(this.regex); diff --git a/src/embeds/spotify.ts b/src/embeds/spotify.ts index dd21ac1..bf9c8dc 100644 --- a/src/embeds/spotify.ts +++ b/src/embeds/spotify.ts @@ -3,6 +3,7 @@ import { EmbedBase } from "./embedBase"; export class SpotifyEmbed extends EmbedBase { name: SupportedWebsites = "Spotify"; + hostnames = ["open.spotify.com", "play.spotify.com"]; regex = new RegExp(/https:\/\/(?:open|play|www)\.spotify\.com\/(\w+)\/(\w+)(?:\?highlight=spotify:track:(\w+))?/); createEmbed(url: string): HTMLElement { diff --git a/src/embeds/steam.ts b/src/embeds/steam.ts index cb676c7..818f488 100644 --- a/src/embeds/steam.ts +++ b/src/embeds/steam.ts @@ -3,6 +3,7 @@ import { EmbedBase } from "./embedBase"; export class SteamEmbed extends EmbedBase { name: SupportedWebsites = "Steam"; + hostnames = ["store.steampowered.com"]; regex = new RegExp(/https:\/\/store\.steampowered\.com\/app\/(\d+)/); createEmbed(url: string): HTMLElement { diff --git a/src/embeds/tiktok.ts b/src/embeds/tiktok.ts index 7277c51..fb910b3 100644 --- a/src/embeds/tiktok.ts +++ b/src/embeds/tiktok.ts @@ -3,6 +3,7 @@ import { EmbedBase } from "./embedBase"; export class TikTokEmbed extends EmbedBase { name: SupportedWebsites = "TikTok"; + hostnames = ["tiktok.com"]; embedOrigin = "https://www.tiktok.com" regex = new RegExp(/https:\/\/www\.tiktok\.com\/@([\w.]+)\/video\/(\d+)/); diff --git a/src/embeds/twitter.ts b/src/embeds/twitter.ts index 17e4916..1005cd0 100644 --- a/src/embeds/twitter.ts +++ b/src/embeds/twitter.ts @@ -3,6 +3,7 @@ import { EmbedBase } from "./embedBase"; export class TwitterEmbed extends EmbedBase { name: SupportedWebsites = "Twitter/X"; + hostnames = ["x.com"]; // Don't parse twitter since Obsidian already handles that. regex = new RegExp(/https:\/\/(?:x)\.com\/(\w+)(?:\/status\/(\w+))?/); embedOrigin = "https://platform.twitter.com"; diff --git a/src/embeds/youtube.ts b/src/embeds/youtube.ts index 4aeee22..0ab296f 100644 --- a/src/embeds/youtube.ts +++ b/src/embeds/youtube.ts @@ -4,6 +4,7 @@ import { EmbedBase } from "./embedBase"; // NOTE: Not maintained since using Obsidian's embeds export class YouTubeEmbed extends EmbedBase { name: SupportedWebsites | "Other" = "Other"; + hostnames = ["youtube.com", "m.youtube.com", "youtu.be", "youtube-nocookie.com"]; // Base from: https://stackoverflow.com/a/61033353/21099543 // Added // - capture group for and support for video types (short, live, etc) diff --git a/src/main.ts b/src/main.ts index a0acb76..406b57a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,7 @@ import { Editor, EditorPosition, MarkdownFileInfo, MarkdownView, Plugin } from 'obsidian'; import { AutoEmbedSettingTab, DEFAULT_SETTINGS, PluginSettings } from 'src/settings-tab'; import SuggestEmbed from 'src/suggestEmbed'; -import { isLinkToImage, isURL, regexUrl } from 'src/utility'; +import { isLinkToImage, getUrlHostname, regexUrl } from 'src/utility'; import { embedField } from './embed-state-field'; import { EmbedManager } from './embeds/embedManager'; @@ -59,10 +59,11 @@ export default class AutoEmbedPlugin extends Plugin { const images = el.querySelectorAll('img'); images.forEach((image) => { - if (image.referrerPolicy !== "no-referrer" || !isURL(image.src) || isLinkToImage(image.src)) + const hostname = getUrlHostname(image.src); + if (image.referrerPolicy !== "no-referrer" || hostname === null || isLinkToImage(image.src)) return; - this.handleImage(image); + this.handleImage(image, hostname); }) // const youTube = el.querySelectorAll('iframe'); @@ -158,7 +159,7 @@ export default class AutoEmbedPlugin extends Plugin { // Check if valid url const clipboardData = e.clipboardData?.getData("text/plain"); - if (!clipboardData || clipboardData === "" || !isURL(clipboardData)) + if (!clipboardData || clipboardData === "" || !getUrlHostname(clipboardData)) return; this.pasteInfo.trigger = true; @@ -167,7 +168,7 @@ export default class AutoEmbedPlugin extends Plugin { // Creates the embed and replaces the Anchor element with it // Returns null if it's unable to convert it to an embed - handleImage(img: HTMLImageElement): HTMLElement | null { + handleImage(img: HTMLImageElement, hostname: string): HTMLElement | null { const alt = img.alt; const noEmbedRegex = /noembed/i; @@ -178,7 +179,7 @@ export default class AutoEmbedPlugin extends Plugin { const src = img.src; - const embedData = EmbedManager.getEmbedData(src, alt); + const embedData = EmbedManager.getEmbedData(hostname, src, alt); // console.log(embedData); if (embedData === null) { diff --git a/src/preview-embed-modal.ts b/src/preview-embed-modal.ts index 9474304..8a33b42 100644 --- a/src/preview-embed-modal.ts +++ b/src/preview-embed-modal.ts @@ -1,5 +1,6 @@ import AutoEmbedPlugin from "src/main"; import { Modal, Setting } from "obsidian"; +import { getUrlHostname } from "./utility"; export class PreviewEmbedModal extends Modal { plugin: AutoEmbedPlugin; @@ -14,12 +15,17 @@ export class PreviewEmbedModal extends Modal { } createEmbed (contentEl: HTMLElement) { + const hostname = getUrlHostname(this.url); + if (hostname === null) + return null; + // Imitate when it's in Reading Mode, replacing the img tag with the embed const readingViewImg = createEl("img"); readingViewImg.src = this.url; readingViewImg.alt = this.options; contentEl.appendChild(readingViewImg); - return this.plugin.handleImage(readingViewImg); + + return this.plugin.handleImage(readingViewImg, hostname); } onOpen(): void { diff --git a/src/utility.ts b/src/utility.ts index 8204512..084f0c6 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -1,16 +1,17 @@ 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 { +export function getUrlHostname(str: string) : string | null { // Returns false if it's an Obsidian internal link. if (str.startsWith("app://")) { - return false; + return null; } try { - new URL(str); - return true; + // Get the hostname and remove "www." if it exists + // Note, no need check for "https://" as this is already the hostname + return new URL(str).hostname.replace(/^www\./, ''); } catch { - return false; + return null; } }