mirror of
https://github.com/gnoxnahte/obsidian-auto-embed.git
synced 2026-07-22 09:50:24 +00:00
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:
parent
f8b3bc59dd
commit
098e0dbdc3
17 changed files with 92 additions and 34 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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+)/);
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
13
src/main.ts
13
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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue