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...
This commit is contained in:
GnoxNahte 2024-10-23 22:51:23 +08:00
parent f8b3bc59dd
commit 098e0dbdc3
17 changed files with 92 additions and 34 deletions

View file

@ -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<DecorationSet>({
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;

View file

@ -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 {

View file

@ -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;
}
}

View file

@ -28,8 +28,9 @@ export class EmbedManager {
plugin: AutoEmbedPlugin;
embedSources: EmbedBase[];
ignoredDomains: RegExp[];
ignoredDomains: string[];
defaultFallbackEmbed: DefaultFallbackEmbed;
hostNameDictionary: Record<string, EmbedBase>;
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);

View file

@ -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 {

View file

@ -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";

View file

@ -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 {

View file

@ -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)

View file

@ -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);

View file

@ -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 {

View file

@ -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 {

View file

@ -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+)/);

View file

@ -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";

View file

@ -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)

View file

@ -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) {

View file

@ -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 {

View file

@ -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;
}
}