Skip to content

Commit

Permalink
use oembed-parse to get the iframe from the source (Youtube, Soundc…
Browse files Browse the repository at this point in the history
…loud etc) WIP (#34)

* feat(oembed): use oembed parse to get the iframe

* test(oembed): add test to getIframe

* feat(oembed): compute aspect ratio

* feat(input): add contextual menu

* feat(readme): update doc

* feat(version): bump to 0.4.0
  • Loading branch information
FHachez committed Dec 20, 2021
1 parent f4cb2f0 commit 6f003ee
Show file tree
Hide file tree
Showing 21 changed files with 257 additions and 116 deletions.
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
# Obsidian Plugin: Convert a URL (e.g. YouTube) into an iframe (preview)
**Transform a YouTube URL into a YouTube preview.**
**Transform any URL into a preview.**

![Demo](images/demo-url-to-preview-3.gif)
![Demo](images/demo-url-to-preview-0.4.0.gif)

Transform a selected URL to an embed view if the website allows it. It offers the possibilitiy to resize the preview.
1. Select an url
2. right click on it (or use the command `Convert to Url Preview`)
3. Click on `Url to Preview/Iframe`.
![contextual menu](images/contextual-menu.png)

The default hotkey is `cmd + shift + i`.

The default hotkey is `cmd + alt + i`.


# Installation
Support for 3rd party plugins is enabled in settings (Obsidian > Settings > Third Party plugin > Safe mode - OFF)
To install this plugin, download zip archive from GitHub releases page. Extract the archive into <vault>/.obsidian/plugins.

# Future improvement
- Find a way to detect when the website doesn't allow cross origins.
- Support more websites.

# Change log

## 0.4.0
- Instead of doing a custom mapping to embed for YouTube, we now rely on the OEmbed standard. Thanks to https://www.npmjs.com/package/oembed-parser
- This allows to preserve the timestamp on Youtube and to get default size for many websites.
- Add contextual menu (right click on a link) ![contextual menu](images/contextual-menu.png)


## 0.3.0
- Simplify the output when using a recent Obsidian download, by leveraging `aspect-ratio` css.

Expand Down
Binary file added images/contextual-menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/demo-url-to-preview-0.4.0.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed images/demo-url-to-preview-3.gif
Binary file not shown.
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "convert-url-to-iframe",
"name": "Convert url to preview (iframe)",
"version": "0.3.0",
"version": "0.4.0",
"description": "Convert an url (ex, youtube) into an iframe (preview)",
"author": "Hachez Floran",
"authorUrl": "https://github.com/FHachez",
Expand Down
70 changes: 70 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "obsidian-convert-url-to-iframe",
"version": "0.3.0",
"version": "0.4.0",
"description": "Convert an url (ex, youtube) into an iframe (preview)",
"main": "main.js",
"scripts": {
Expand All @@ -12,6 +12,8 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@types/dompurify": "^2.3.2",
"@types/jsdom": "^16.2.14",
"@types/node": "^14.14.2",
"@typescript-eslint/eslint-plugin": "^5.2.0",
"@typescript-eslint/parser": "^5.2.0",
Expand All @@ -25,5 +27,9 @@
"ts-jest": "^26.4.3",
"tslib": "^2.0.3",
"typescript": "^4.1.0"
},
"dependencies": {
"dompurify": "^2.3.4",
"oembed-parser": "^2.0.0"
}
}
21 changes: 12 additions & 9 deletions src/components/resizable_iframe_container.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defaultHeight } from "src/constant";
import { AspectRatioType } from "src/types/aspect-ratio";
import { swapRatio } from "src/utils/ratio_swapper";
import { addAspectRatio, swapRatio } from "src/utils/ratio.utils";

export interface IResizableIframeContainerOutput {
iframeContainer: HTMLElement,
Expand All @@ -10,18 +10,20 @@ export interface IResizableIframeContainerOutput {
}


export function createIframeContainerEl(contentEl: HTMLElement, url: string): IResizableIframeContainerOutput{
export function createIframeContainerEl(contentEl: HTMLElement, iframeHtml: string): IResizableIframeContainerOutput{
// Container to keep a min height for the iframe to keep the content visible
const iframeContainer = contentEl.createEl('div');
iframeContainer.className = "iframe__container space-y"

// Inline styling to make sure that the created iframe will keep the style even without the plugin
const iframe = iframeContainer.createEl('iframe');
iframe.src = url;
iframe.allow = "fullscreen"
iframe.style.height = '100%';
iframe.style.width = '100%';
iframe.style.setProperty('aspect-ratio', '16/9');
const fragment = document.createElement('template');
fragment.innerHTML = iframeHtml;

const iframe = fragment.content.firstChild as HTMLIFrameElement;
iframeContainer.appendChild(iframe);

console.log(iframe.outerHTML)
addAspectRatio(iframe);

const resetToDefaultWidth= () => {
iframe.style.width = '100%';
Expand All @@ -30,6 +32,7 @@ export function createIframeContainerEl(contentEl: HTMLElement, url: string): IR

resetToDefaultWidth();

console.log(iframeHtml, iframe)

return {
iframeContainer,
Expand Down Expand Up @@ -108,4 +111,4 @@ export function createIframeContainerElLegacy(contentEl: HTMLElement, url: strin
iframeContainer.style.height = ratioContainer.offsetHeight + 'px';
},
};
}
}
8 changes: 4 additions & 4 deletions src/configure_iframe_modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import { doesSupportAspectRatio } from './constant';


export class ConfigureIframeModal extends Modal {
url: string;
iframeHtml: string;
editor: any;

constructor(app: App, url: string, editor: any) {
constructor(app: App, iframeHtml: string, editor: any) {
super(app);
this.url = url;
this.iframeHtml = iframeHtml;
this.editor = editor;

// Allow the modal to grow in width
Expand All @@ -33,7 +33,7 @@ export class ConfigureIframeModal extends Modal {

// Electron < 12 doesn't support the aspect ratio. We need to use a fancy div container with a padding bottom
const { iframeContainer, outputHtml, resetToDefaultWidth, updateAspectRatio } = doesSupportAspectRatio ?
createIframeContainerEl(contentEl, this.url) : createIframeContainerElLegacy(contentEl, this.url);
createIframeContainerEl(contentEl, this.iframeHtml) : createIframeContainerElLegacy(contentEl, this.iframeHtml);
const widthCheckbox = createShouldUseDefaultWidthButton(container, resetToDefaultWidth);
const aspectRatioInput = createAspectRatioInput(container ,updateAspectRatio);

Expand Down
2 changes: 1 addition & 1 deletion src/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ declare namespace process {
export const defaultHeight = "100px"

const electronMajorVersion = process.versions?.electron?.split('.')[0]
export const doesSupportAspectRatio = +(electronMajorVersion) >= 12;
export const doesSupportAspectRatio = +(electronMajorVersion) >= 12;
51 changes: 40 additions & 11 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@

import { Notice, Plugin } from 'obsidian';
import { Editor, MarkdownView, Menu, Notice, Plugin } from 'obsidian';

import { isUrl, updateUrlIfYoutube } from './utils/url_converter';
import { getIframe} from './utils/iframe_generator.utils';
import { ConfigureIframeModal } from './configure_iframe_modal';
import { isUrl } from './utils/url.utils';

export default class FormatNotionPlugin extends Plugin {
async onload() {
console.log('Loading obsidian-convert-url-to-iframe');
this.addCommand({
id: "url-to-iframe",
name: "URL to iframe/preview",
name: "URL to Preview/Iframe",
callback: () => this.urlToIframe(),
hotkeys: [
{
Expand All @@ -18,21 +19,49 @@ export default class FormatNotionPlugin extends Plugin {
},
],
});

// Editor mode (right click on text)
this.registerEvent(this.app.workspace.on('editor-menu',
(menu: Menu, _: Editor, view: MarkdownView) => {
const url = this.getCleanedUrl();
if (url) {
menu.addItem((item) => {
item.setTitle("Url to Preview/Iframe")
.setIcon("create-new")
.onClick((_) => {
this.urlToIframe(url);
});
});
}
}));

}

urlToIframe(): void {
async urlToIframe(inputUrl?: string): Promise<void> {
const activeLeaf: any = this.app.workspace.activeLeaf;
const editor = activeLeaf.view.sourceMode.cmEditor;
const selectedText = editor.somethingSelected()
? editor.getSelection()
: false;

if (selectedText && isUrl(selectedText)) {
const url = updateUrlIfYoutube(selectedText)
const modal = new ConfigureIframeModal(this.app, url, editor)
const url = inputUrl || this.getCleanedUrl()

if (url) {
const iframeHtml = await getIframe(url)
const modal = new ConfigureIframeModal(this.app, iframeHtml, editor)
modal.open();
} else {
new Notice('Select a URL to convert to an iframe.');
new Notice('Select a URL to convert to an preview/iframe.');
}
}

private getCleanedUrl(): string {
const activeLeaf: any = this.app.workspace.activeLeaf;
const editor = activeLeaf.view.sourceMode.cmEditor;
const selectedText: string = editor.somethingSelected() ? editor.getSelection() : null;
const cleanedText = selectedText?.trim();

if (selectedText && isUrl(cleanedText)) {
return cleanedText
}
return null;
}

}
2 changes: 1 addition & 1 deletion src/types/aspect-ratio.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@

export type AspectRatioType = `${number}/${number}` | 'none';
export type AspectRatioType = `${number}/${number}` | 'none';
19 changes: 19 additions & 0 deletions src/utils/__tests__/iframe_generator.utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as DOMPurify from 'dompurify'
import { JSDOM } from 'jsdom';

import { getIframeGeneratorFromSanitize} from '../iframe_generator.utils';

describe('getIframeNode', () => {
const getIframe = getIframeGeneratorFromSanitize(DOMPurify(new JSDOM('').window as unknown as Window).sanitize);

const inputToExpectedOutput = [
["https://github.com/", '<iframe src=https://github.com/ allow="fullscreen" style="height:100%;width:100%; aspect-ratio=16/9;"></iframe>'],
["https://www.youtube.com/watch?v=zU2-QMP5e5g", '<iframe src="https://www.youtube.com/embed/zU2-QMP5e5g?feature=oembed" height="113" width="200"></iframe>'],
["https://soundcloud.com/marshmellomusic/sets/marshmello-x-lil-dusty-g", '<iframe src="https://w.soundcloud.com/player/?visual=true&amp;url=https%3A%2F%2Fapi.soundcloud.com%2Fplaylists%2F1338967234&amp;show_artwork=true" height="450" width="100%"></iframe>'],
]
it.each(inputToExpectedOutput)('should correctly parse "%s"', async (input: string, expected) => {
const output = await getIframe(input);

expect(output).toStrictEqual(expected)
})
})
Loading

0 comments on commit 6f003ee

Please sign in to comment.