Skip to content

Commit 997ad4c

Browse files
authored
Merge pull request samwarnick#47 from udbhav-s/main
Add a generic link preview source
2 parents 464865d + c0e075b commit 997ad4c

File tree

8 files changed

+233
-9
lines changed

8 files changed

+233
-9
lines changed

.gitignore

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
genericPreviewCache.json
2+
13
# VSCode
24
.vscode
35

@@ -12,5 +14,5 @@ main.js
1214
# obsidian
1315
data.json
1416

15-
.DS_Store
16-
.nova
17+
.DS_Store
18+
.nova

embeds/generic-preview.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { requestUrl } from "obsidian";
2+
import { EmbedSource, EnableEmbedKey } from "./";
3+
import { getPreviewFromContent } from "link-preview-js";
4+
import { PluginSettings } from "settings";
5+
import SimpleEmbedsPlugin from "main";
6+
7+
export class GenericPreviewEmbed implements EmbedSource {
8+
name = "Generic Preview";
9+
enabledKey: EnableEmbedKey = "replaceGenericLinks";
10+
// unmatchable regex
11+
// this source is used as a fallback when other sources fail to match
12+
regex = new RegExp("(?!)");
13+
14+
createEmbed(
15+
link: string,
16+
container: HTMLElement,
17+
settings: Readonly<PluginSettings>,
18+
currentTheme: "light" | "dark",
19+
plugin: SimpleEmbedsPlugin,
20+
) {
21+
const preview = document.createElement("a");
22+
preview.setAttr("href", link);
23+
preview.classList.add("preview");
24+
preview.textContent = "Loading preview...";
25+
26+
const loadPreview = async () => {
27+
let metadata;
28+
29+
// await cache file load if not available yet
30+
if (!plugin.genericPreviewCache) await plugin.cacheFileLoadPromise;
31+
32+
if (settings.useCacheForGenericLinks && link in plugin.genericPreviewCache) {
33+
metadata = plugin.genericPreviewCache[link];
34+
} else {
35+
const res = await requestUrl({ url: link });
36+
metadata = await getPreviewFromContent({
37+
headers: res.headers,
38+
data: res.text,
39+
url: link
40+
});
41+
42+
if (settings.useCacheForGenericLinks && "title" in metadata) {
43+
plugin.saveGenericPreviewCache(link, metadata);
44+
}
45+
}
46+
47+
if (!("title" in metadata)) return;
48+
49+
preview.innerHTML =
50+
String.raw`
51+
<div class="image-container">
52+
${ metadata.images.length ? String.raw`<img src="${metadata.images[0]}" />` : "" }
53+
</div>
54+
<div class="content">
55+
<div class="title">${metadata.title}</div>
56+
<div class="description">${metadata.description ? metadata.description : ""}</div>
57+
</div>
58+
`;
59+
}
60+
61+
try {
62+
loadPreview();
63+
} catch {
64+
preview.textContent = "Could not load preview";
65+
}
66+
67+
container.appendChild(preview);
68+
container.classList.add("generic-preview");
69+
return container;
70+
}
71+
}

embeds/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import SimpleEmbedsPlugin from "main";
12
import { Setting } from "obsidian";
23
import { EnableEmbeds, PluginSettings } from "../settings";
34

@@ -11,6 +12,7 @@ export interface EmbedSource {
1112
container: HTMLElement,
1213
settings: Readonly<PluginSettings>,
1314
currentTheme: "light" | "dark",
15+
plugin: SimpleEmbedsPlugin,
1416
): HTMLElement;
1517
afterAllEmbeds?(): void;
1618
updateTheme?(
@@ -37,3 +39,4 @@ export * from "./reddit";
3739
export * from "./twitter";
3840
export * from "./vimeo";
3941
export * from "./youtube";
42+
export * from "./generic-preview";

main.ts

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,14 @@ import {
1313
TwitterEmbed,
1414
VimeoEmbed,
1515
YouTubeEmbed,
16+
GenericPreviewEmbed,
1617
} from "./embeds";
17-
import { debounce, Debouncer, MarkdownView, Plugin } from "obsidian";
18-
import { DEFAULT_SETTINGS, PluginSettings } from "./settings";
18+
import { debounce, Debouncer, MarkdownView, Plugin, TFile } from "obsidian";
19+
import {
20+
DEFAULT_SETTINGS,
21+
GenericPreviewMetadata,
22+
PluginSettings,
23+
} from "./settings";
1924
import { SimpleEmbedPluginSettingTab } from "./settings-tab";
2025
import { buildSimpleEmbedsViewPlugin } from "./view-plugin";
2126

@@ -39,6 +44,15 @@ export default class SimpleEmbedsPlugin extends Plugin {
3944
processedMarkdown: Debouncer<[]>;
4045
currentTheme: "dark" | "light";
4146

47+
genericPreviewEmbed = new GenericPreviewEmbed();
48+
genericPreviewCache = null as {
49+
[url: string]: GenericPreviewMetadata;
50+
} | null;
51+
genericPreviewCacheFile =
52+
this.app.vault.configDir +
53+
"/plugins/obsidian-simple-embeds/genericPreviewCache.json";
54+
cacheFileLoadPromise = null as Promise<void>;
55+
4256
async onload() {
4357
console.log(`Loading ${this.manifest.name} v${this.manifest.version}`);
4458
await this.loadSettings();
@@ -57,7 +71,7 @@ export default class SimpleEmbedsPlugin extends Plugin {
5771

5872
this.registerMarkdownPostProcessor((el, ctx) => {
5973
const anchors = el.querySelectorAll(
60-
"a.external-link",
74+
"a.external-link"
6175
) as NodeListOf<HTMLAnchorElement>;
6276
anchors.forEach((anchor) => {
6377
this._handleAnchor(anchor);
@@ -77,8 +91,27 @@ export default class SimpleEmbedsPlugin extends Plugin {
7791
});
7892
});
7993
}
80-
}),
94+
})
8195
);
96+
97+
// Load file for generic preview cache
98+
const loadCacheFile = async () => {
99+
if (
100+
!(await this.app.vault.adapter.exists(this.genericPreviewCacheFile))
101+
) {
102+
await this.app.vault.create(this.genericPreviewCacheFile, "{}");
103+
}
104+
try {
105+
const contents = JSON.parse(
106+
await this.app.vault.adapter.read(this.genericPreviewCacheFile)
107+
);
108+
this.genericPreviewCache = contents;
109+
} catch (e) {
110+
console.error("Error reading generic preview cache file");
111+
console.error(e);
112+
}
113+
};
114+
this.cacheFileLoadPromise = loadCacheFile();
82115
}
83116

84117
onunload() {
@@ -100,6 +133,19 @@ export default class SimpleEmbedsPlugin extends Plugin {
100133
});
101134
}
102135

136+
async saveGenericPreviewCache(
137+
link: string,
138+
metadata: GenericPreviewMetadata
139+
) {
140+
if (this.genericPreviewCacheFile) {
141+
this.genericPreviewCache[link] = metadata;
142+
await this.app.vault.adapter.write(
143+
this.genericPreviewCacheFile,
144+
JSON.stringify(this.genericPreviewCache)
145+
);
146+
}
147+
}
148+
103149
private _getCurrentTheme(): "dark" | "light" {
104150
return document.body.classList.contains("theme-dark") ? "dark" : "light";
105151
}
@@ -117,7 +163,7 @@ export default class SimpleEmbedsPlugin extends Plugin {
117163

118164
const replaceWithEmbed = this.shouldReplaceWithEmbed(
119165
a.innerText,
120-
isWithinText,
166+
isWithinText
121167
);
122168
const fullWidth = a.innerText.includes("|fullwidth");
123169
// Remove any allowed properties:
@@ -141,9 +187,21 @@ export default class SimpleEmbedsPlugin extends Plugin {
141187
href,
142188
fullWidth,
143189
this.settings.centerEmbeds,
144-
this.settings.keepLinksInPreview,
190+
this.settings.keepLinksInPreview
145191
);
146192
this._insertEmbed(a, embed);
193+
} else {
194+
if (this.settings.replaceGenericLinks) {
195+
// fall back to creating a generic embed
196+
const embed = this.createEmbed(
197+
this.genericPreviewEmbed,
198+
href,
199+
fullWidth,
200+
this.settings.centerEmbeds,
201+
this.settings.keepLinksInPreview
202+
);
203+
this._insertEmbed(a, embed);
204+
}
147205
}
148206
}
149207

@@ -162,7 +220,7 @@ export default class SimpleEmbedsPlugin extends Plugin {
162220
link: string,
163221
fullWidth: boolean,
164222
centered: boolean,
165-
keepLinks: boolean,
223+
keepLinks: boolean
166224
) {
167225
const container = document.createElement("div");
168226
container.classList.add("embed-container");
@@ -171,6 +229,7 @@ export default class SimpleEmbedsPlugin extends Plugin {
171229
container,
172230
this.settings,
173231
this.currentTheme,
232+
this
174233
);
175234
if (fullWidth) {
176235
embed.classList.add("full-width");

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,8 @@
2626
"ts-jest": "^27.0.7",
2727
"tslib": "^2.2.0",
2828
"typescript": "^4.2.4"
29+
},
30+
"dependencies": {
31+
"link-preview-js": "^3.0.3"
2932
}
3033
}

settings-tab.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,39 @@ export class SimpleEmbedPluginSettingTab extends PluginSettingTab {
4343
});
4444
});
4545

46+
// Settings for generic link previews
47+
containerEl.createEl("h3", { text: "Generic Link Previews" });
48+
49+
new Setting(containerEl)
50+
.setName("Show generic previews for links")
51+
.addToggle((toggle) => {
52+
toggle
53+
.setValue(this.plugin.settings.replaceGenericLinks)
54+
.onChange(async (enabled) => {
55+
await this.saveSettings({ replaceGenericLinks: enabled });
56+
});
57+
});
58+
new Setting(containerEl)
59+
.setName("Use a cache for link preview metadata")
60+
.addToggle((toggle) => {
61+
toggle
62+
.setValue(this.plugin.settings.useCacheForGenericLinks)
63+
.onChange(async (enabled) => {
64+
await this.saveSettings({ useCacheForGenericLinks: enabled });
65+
});
66+
});
67+
new Setting(containerEl)
68+
.setName("Clear link preview metadata cache")
69+
.addButton((button) => {
70+
button
71+
.setButtonText("Clear")
72+
.onClick(async () => {
73+
await this.app.vault.adapter.write(this.plugin.genericPreviewCacheFile, "{}");
74+
this.plugin.genericPreviewCache = {};
75+
await this.plugin.saveSettings({});
76+
});
77+
});
78+
4679
// Any additional settings for embed sources.
4780
containerEl.createEl("h3", { text: "Appearance" });
4881

settings.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface EnableEmbeds {
1212
replaceTwitterLinks: boolean;
1313
replaceVimeoLinks: boolean;
1414
replaceYouTubeLinks: boolean;
15+
replaceGenericLinks: boolean;
1516
}
1617
export interface TwitterAppearanceSettings {
1718
twitterTheme: "auto" | "dark" | "light";
@@ -34,11 +35,25 @@ export interface AdvancedSettings {
3435
disableAutomaticEmbeds: boolean;
3536
}
3637

38+
export interface GenericPreviewMetadata {
39+
title: string;
40+
description: string;
41+
images: string[];
42+
}
43+
44+
export interface GenericPreviewSettings {
45+
useCacheForGenericLinks: boolean;
46+
// genericPreviewCache: {
47+
// [url: string]: GenericPreviewMetadata;
48+
// };
49+
}
50+
3751
export interface PluginSettings
3852
extends EnableEmbeds,
3953
TwitterAppearanceSettings,
4054
CodePenAppearanceSettings,
4155
RedditAppearanceSettings,
56+
GenericPreviewSettings,
4257
AdvancedSettings {}
4358

4459
export const DEFAULT_SETTINGS: PluginSettings = {
@@ -56,6 +71,9 @@ export const DEFAULT_SETTINGS: PluginSettings = {
5671
replaceVimeoLinks: true,
5772
replaceYouTubeLinks: true,
5873

74+
replaceGenericLinks: false,
75+
useCacheForGenericLinks: true,
76+
5977
twitterTheme: "auto",
6078

6179
codepenTheme: "auto",

styles.css

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,41 @@
8080
left: calc(50% - 75px);
8181
}
8282

83+
/* Generic Preview */
84+
.embed-container.generic-preview .preview {
85+
text-decoration: none;
86+
color: inherit;
87+
background: var(--background-secondary);
88+
border: 1px solid var(--background-modifier-border);;
89+
display: flex;
90+
flex-direction: row;
91+
align-items: center;
92+
justify-content: flex-start;
93+
margin: 0.5rem 0;
94+
}
95+
.embed-container.generic-preview .preview:hover {
96+
background: var(--background-secondary-alt);
97+
cursor: pointer;
98+
}
99+
100+
.embed-container.generic-preview .preview .image-container {
101+
max-width: 100px;
102+
}
103+
104+
.embed-container.generic-preview .preview .content {
105+
margin: 0.5rem;
106+
margin-left: 1rem;
107+
}
108+
109+
.embed-container.generic-preview .preview .content .title {
110+
font-size: 1.05rem;
111+
font-weight: 600;
112+
}
113+
114+
.embed-container.generic-preview .preview .content .description {
115+
color: var(--text-muted);
116+
}
117+
83118
/* Settings */
84119
.simple-embeds-settings details > summary {
85120
display: flex;

0 commit comments

Comments
 (0)