Skip to content
Snippets Groups Projects
Commit 8830b215 authored by Bartłomiej Górnicki's avatar Bartłomiej Górnicki
Browse files

feat: rich embed for Youtube shorts

parent 88def131
No related branches found
No related tags found
1 merge request!14Update current embedders with modern links and iframes formats
......@@ -48,24 +48,6 @@ describe('DefaultRender', () => {
expected:
'<div class="videoWrapper"><iframe frameborder="0" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen" src="https://player.vimeo.com/video/174544848" width="640" height="480"></iframe></div>'
},
{
name: 'Embeds correctly youtube video via paste',
raw: '<iframe width="560" height="315" src="https://www.youtube.com/embed/0nFkmd-A7jA" frameborder="0" allowfullscreen></iframe>',
expected:
'<div class="videoWrapper"><iframe width="640" height="480" src="https://www.youtube.com/embed/0nFkmd-A7jA" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen" frameborder="0"></iframe></div>'
},
{
name: 'Embeds correctly youtube video via youtube.com link',
raw: 'https://www.youtube.com/embed/0nFkmd-A7jA',
expected:
'<p><div class="videoWrapper"><iframe width="640" height="480" src="https://www.youtube.com/embed/0nFkmd-A7jA" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen" frameborder="0"></iframe></div></p>'
},
{
name: 'Embeds correctly youtube video via youtu.be link',
raw: 'https://www.youtu.be/0nFkmd-A7jA',
expected:
'<p><div class="videoWrapper"><iframe width="640" height="480" src="https://www.youtube.com/embed/0nFkmd-A7jA" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen" frameborder="0"></iframe></div></p>'
},
{
name: 'Allows links embedded via <a> tags',
raw: '<a href="https://hive.blog/utopian-io/@blockchainstudio/drugswars-revenue-and-transaction-analysis" class="hive-test">Drugwars - revenue and transaction analysis</a>',
......@@ -164,6 +146,42 @@ describe('DefaultRender', () => {
raw: '<iframe src="https://open.spotify.com/embed/artist/1zLvUhumbFIEdfxYQcgUxk" width="640" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>',
expected:
'<div class="videoWrapper"><iframe src="https://open.spotify.com/embed/artist/1zLvUhumbFIEdfxYQcgUxk" width="640" height="480" frameborder="0" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen"></iframe></div>'
},
{
name: 'Youtube link with www should be embedded correctly',
raw: 'https://www.youtube.com/watch?v=0nFkmd-A7jA',
expected:
'<p><div class="videoWrapper"><iframe width="640" height="480" src="https://www.youtube.com/embed/0nFkmd-A7jA" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen" frameborder="0"></iframe></div></p>'
},
{
name: 'Youtube link without www should be embedded correctly',
raw: 'https://youtube.com/watch?v=0nFkmd-A7jA',
expected:
'<p><div class="videoWrapper"><iframe width="640" height="480" src="https://www.youtube.com/embed/0nFkmd-A7jA" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen" frameborder="0"></iframe></div></p>'
},
{
name: 'Youtube link with embed should be embedded correctly',
raw: 'https://www.youtube.com/embed/0nFkmd-A7jA',
expected:
'<p><div class="videoWrapper"><iframe width="640" height="480" src="https://www.youtube.com/embed/0nFkmd-A7jA" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen" frameborder="0"></iframe></div></p>'
},
{
name: 'Youtube shorted link with watch should be embedded correctly',
raw: 'https://youtu.be/watch?v=0nFkmd-A7jA',
expected:
'<p><div class="videoWrapper"><iframe width="640" height="480" src="https://www.youtube.com/embed/0nFkmd-A7jA" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen" frameborder="0"></iframe></div></p>'
},
{
name: 'Youtube shorted link should be embedded correctly',
raw: 'https://youtu.be/0nFkmd-A7jA',
expected:
'<p><div class="videoWrapper"><iframe width="640" height="480" src="https://www.youtube.com/embed/0nFkmd-A7jA" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen" frameborder="0"></iframe></div></p>'
},
{
name: 'Youtube embed via iframe should be embedded correctly',
raw: '<iframe width="560" height="315" src="https://www.youtube.com/embed/0nFkmd-A7jA" frameborder="0" allowfullscreen></iframe>',
expected:
'<div class="videoWrapper"><iframe width="640" height="480" src="https://www.youtube.com/embed/0nFkmd-A7jA" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen" frameborder="0"></iframe></div>'
}
];
......@@ -175,8 +193,8 @@ describe('DefaultRender', () => {
const renderedNode = JSDOM.fragment(rendered);
const comparisonNode = JSDOM.fragment(test.expected);
Log.log().debug('rendered', rendered);
Log.log().debug('expected', test.expected);
Log.log().info('rendered', rendered);
Log.log().info('expected', test.expected);
expect(renderedNode.isEqualNode(comparisonNode)).to.be.equal(true);
})
......
......@@ -8,7 +8,7 @@ describe('SpotifyEmbedder', () => {
description: 'should properly return metadata for spotify playlist',
input: 'https://open.spotify.com/playlist/1zLvUhumbFIEdfxYQcgUxk',
expected: {
canonical: 'https://open.spotify.com/playlist/1zLvUhumbFIEdfxYQcgUxk',
image: 'https://open.spotify.com/playlist/1zLvUhumbFIEdfxYQcgUxk',
id: 'embed/playlist/1zLvUhumbFIEdfxYQcgUxk',
url: 'https://open.spotify.com/playlist/1zLvUhumbFIEdfxYQcgUxk'
}
......@@ -17,7 +17,7 @@ describe('SpotifyEmbedder', () => {
description: 'should properly return metadata for spotify show',
input: 'https://open.spotify.com/show/1zLvUhumbFIEdfxYQcgUxk',
expected: {
canonical: 'https://open.spotify.com/show/1zLvUhumbFIEdfxYQcgUxk',
image: 'https://open.spotify.com/show/1zLvUhumbFIEdfxYQcgUxk',
id: 'embed-podcast/show/1zLvUhumbFIEdfxYQcgUxk',
url: 'https://open.spotify.com/show/1zLvUhumbFIEdfxYQcgUxk'
}
......@@ -26,7 +26,7 @@ describe('SpotifyEmbedder', () => {
description: 'should properly return metadata for spotify episode',
input: 'https://open.spotify.com/episode/1zLvUhumbFIEdfxYQcgUxk',
expected: {
canonical: 'https://open.spotify.com/episode/1zLvUhumbFIEdfxYQcgUxk',
image: 'https://open.spotify.com/episode/1zLvUhumbFIEdfxYQcgUxk',
id: 'embed-podcast/episode/1zLvUhumbFIEdfxYQcgUxk',
url: 'https://open.spotify.com/episode/1zLvUhumbFIEdfxYQcgUxk'
}
......@@ -35,7 +35,7 @@ describe('SpotifyEmbedder', () => {
description: 'should properly return metadata for spotify album',
input: 'https://open.spotify.com/album/1zLvUhumbFIEdfxYQcgUxk',
expected: {
canonical: 'https://open.spotify.com/album/1zLvUhumbFIEdfxYQcgUxk',
image: 'https://open.spotify.com/album/1zLvUhumbFIEdfxYQcgUxk',
id: 'embed/album/1zLvUhumbFIEdfxYQcgUxk',
url: 'https://open.spotify.com/album/1zLvUhumbFIEdfxYQcgUxk'
}
......@@ -44,7 +44,7 @@ describe('SpotifyEmbedder', () => {
description: 'should properly return metadata for spotify track',
input: 'https://open.spotify.com/track/1zLvUhumbFIEdfxYQcgUxk',
expected: {
canonical: 'https://open.spotify.com/track/1zLvUhumbFIEdfxYQcgUxk',
image: 'https://open.spotify.com/track/1zLvUhumbFIEdfxYQcgUxk',
id: 'embed/track/1zLvUhumbFIEdfxYQcgUxk',
url: 'https://open.spotify.com/track/1zLvUhumbFIEdfxYQcgUxk'
}
......@@ -53,7 +53,7 @@ describe('SpotifyEmbedder', () => {
description: 'should properly return metadata for spotify artist',
input: 'https://open.spotify.com/artist/1zLvUhumbFIEdfxYQcgUxk',
expected: {
canonical: 'https://open.spotify.com/artist/1zLvUhumbFIEdfxYQcgUxk',
image: 'https://open.spotify.com/artist/1zLvUhumbFIEdfxYQcgUxk',
id: 'embed/artist/1zLvUhumbFIEdfxYQcgUxk',
url: 'https://open.spotify.com/artist/1zLvUhumbFIEdfxYQcgUxk'
}
......@@ -62,12 +62,22 @@ describe('SpotifyEmbedder', () => {
description: 'should return undefined for invalid input',
input: 'https://open.spotify.com/invalid/1zLvUhumbFIEdfxYQcgUxk',
expected: undefined
},
{
description: 'should return undefined for empty input',
input: '',
expected: undefined
},
{
description: 'should return undefined for undefined input',
input: undefined,
expected: undefined
}
].forEach((test) => {
it(test.description, () => {
const embedder = new SpotifyEmbedder();
const node = new JSDOM().window.document.createElement('object');
node.data = test.input;
node.data = test.input as string;
const result = embedder.getEmbedMetadata(node);
expect(result).to.be.deep.equal(test.expected);
......@@ -82,4 +92,19 @@ describe('SpotifyEmbedder', () => {
expect(result).to.be.equal(expected);
});
it('should log exceptions and return undefined', () => {
// mock SpotifyEmbedder.extractMetadata to throw an exception
const extractMetadata = SpotifyEmbedder['extractMetadata'];
SpotifyEmbedder['extractMetadata'] = () => {
throw new Error('mock error');
};
const embedder = new SpotifyEmbedder();
const node = new JSDOM().window.document.createElement('object');
node.data = 'https://open.spotify.com/playlist/1zLvUhumbFIEdfxYQcgUxk';
const result = embedder.getEmbedMetadata(node);
expect(result).to.be.undefined;
SpotifyEmbedder['extractMetadata'] = extractMetadata;
});
});
......@@ -36,7 +36,9 @@ export class SpotifyEmbedder extends AbstractEmbedder {
return undefined;
}
return {
...metadata
id: metadata.id,
url: metadata.url,
image: metadata.canonical
};
} catch (error) {
Log.log().error(error);
......
import {expect} from 'chai';
import {JSDOM} from 'jsdom';
import {YoutubeEmbedder} from './YoutubeEmbedder';
describe('YoutubeEmbedder', () => {
[
// different formats of the same youtube video link
'https://www.youtube.com/watch?v=umvcUpmIie8',
'https://youtube.com/watch?v=umvcUpmIie8',
'https://youtu.be/umvcUpmIie8',
'https://youtu.be/watch?v=umvcUpmIie8',
'https://www.youtube.com/embed/umvcUpmIie8',
'https://youtube.com/embed/umvcUpmIie8'
].forEach((input) => {
it('should properly return metadata for youtube video link', () => {
const expected = {
id: 'umvcUpmIie8',
url: input,
image: 'https://img.youtube.com/vi/umvcUpmIie8/0.jpg'
};
const embedder = new YoutubeEmbedder();
const node = new JSDOM().window.document.createElement('object');
node.data = input;
const metadata = embedder.getEmbedMetadata(node);
expect(metadata).to.be.deep.equal(expected);
});
});
[
// youtube shorts
'https://www.youtube.com/shorts/_R4ScrD0O8c',
'https://youtube.com/shorts/_R4ScrD0O8c'
].forEach((input) => {
it('should properly return metadata for youtube shorts link', () => {
const expected = {
id: '_R4ScrD0O8c',
url: input,
image: 'https://img.youtube.com/vi/_R4ScrD0O8c/0.jpg'
};
const embedder = new YoutubeEmbedder();
const node = new JSDOM().window.document.createElement('object');
node.data = input;
const metadata = embedder.getEmbedMetadata(node);
expect(metadata).to.be.deep.equal(expected);
});
});
[
// invalid inputs
'https://www.youtube.com/watch?v=',
'https://youtube.com/watch?v=',
'https://youtu.be/',
'https://youtu.be/watch?v=',
'https://www.youtube.com/embed/',
'https://youtube.com/embed/',
'https://oauth.com/login/redirect?=youtube.com',
'https://oauth.com/login/redirect?=www.youtube.com'
].forEach((input) => {
it('should return undefined for invalid input', () => {
const embedder = new YoutubeEmbedder();
const node = new JSDOM().window.document.createElement('object');
node.data = input;
const metadata = embedder.getEmbedMetadata(node);
expect(metadata).to.be.undefined;
});
});
it('should return undefined for empty input', () => {
const input = '';
const embedder = new YoutubeEmbedder();
const node = new JSDOM().window.document.createElement('object');
node.data = input;
const metadata = embedder.getEmbedMetadata(node);
expect(metadata).to.be.undefined;
});
it('should log error and return undefined for invalid input', () => {
// mock SpotifyEmbedder.extractMetadata to throw an exception
const extractMetadata = YoutubeEmbedder['getYoutubeMetadataFromLink'];
YoutubeEmbedder['getYoutubeMetadataFromLink'] = () => {
throw new Error('mock error');
};
const embedder = new YoutubeEmbedder();
const node = new JSDOM().window.document.createElement('object');
node.data = 'https://open.spotify.com/playlist/1zLvUhumbFIEdfxYQcgUxk';
const result = embedder.getEmbedMetadata(node);
expect(result).to.be.undefined;
YoutubeEmbedder['getYoutubeMetadataFromLink'] = extractMetadata;
});
});
import {Log} from '../../../../Log';
import linksRe from '../utils/Links';
import {AbstractEmbedder, EmbedMetadata} from './AbstractEmbedder';
export class YoutubeEmbedder extends AbstractEmbedder {
public static getYoutubeMetadataFromLink(data: string): {id: string; url: string; thumbnail: string} | null {
public type = 'youtube';
private static readonly linkRegex =
/https?:\/\/(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/watch\?v=|youtu.be\/[^watch]|youtube\.com\/(embed|shorts)\/)([A-Za-z0-9_-]+)[^ ]*/i;
private static readonly idRegex = /(?:youtube\.com\/watch\?v=|youtu\.be\/watch\?v=|youtu.be\/|youtube\.com\/(embed|shorts)\/)([A-Za-z0-9_-]+)/i;
public static getYoutubeMetadataFromLink(data: string): {id: string; url: string; thumbnail: string} | undefined {
if (!data) {
return null;
return undefined;
}
const m1 = data.match(linksRe.youTube);
const url = m1 ? m1[0] : null;
const m1 = data.match(YoutubeEmbedder.linkRegex);
const url = m1 ? m1[0] : undefined;
if (!url) {
return null;
return undefined;
}
const m2 = url.match(linksRe.youTubeId);
const id = m2 && m2.length >= 2 ? m2[1] : null;
const m2 = url.match(YoutubeEmbedder.idRegex);
const id = m2 && m2.length >= 2 ? m2[2] : undefined;
if (!id) {
return null;
return undefined;
}
return {
......@@ -27,8 +32,6 @@ export class YoutubeEmbedder extends AbstractEmbedder {
};
}
public type = 'youtube';
public getEmbedMetadata(child: HTMLObjectElement): EmbedMetadata | undefined {
try {
const metadata = YoutubeEmbedder.getYoutubeMetadataFromLink(child.data);
......@@ -36,7 +39,9 @@ export class YoutubeEmbedder extends AbstractEmbedder {
return undefined;
}
return {
...metadata
id: metadata.id,
url: metadata.url,
image: metadata.thumbnail
};
} catch (error) {
Log.log().error(error);
......
......@@ -23,7 +23,6 @@ export const any = (flags = 'i') => new RegExp(urlSet(), flags);
// TODO verify if we should pass baseUrl here
export const local = (flags = 'i') => new RegExp(urlSet({domain: '(?:localhost|(?:.*\\.)?hive.blog)'}), flags);
export const remote = (flags = 'i') => new RegExp(urlSet({domain: `(?!localhost|(?:.*\\.)?hive.blog)${domainPath}`}), flags);
export const youTube = (flags = 'i') => new RegExp(urlSet({domain: '(?:(?:.*.)?(youtube\\.com|youtu\\.be))'}), flags);
export const image = (flags = 'i') => new RegExp(urlSet({path: imagePath}), flags);
export const imageFile = (flags = 'i') => new RegExp(imagePath, flags);
......@@ -33,8 +32,6 @@ export default {
remote: remote(),
image: image(),
imageFile: imageFile(),
youTube: youTube(),
youTubeId: /(?:youtube.com\/watch\?v=|(?:youtu.be\/)|(?:youtube.com\/embed\/))([A-Za-z0-9_-]+)/i,
vimeo: /https?:\/\/(?:vimeo.com\/|player.vimeo.com\/video\/)([0-9]+)\/*/,
vimeoId: /(?:vimeo.com\/|player.vimeo.com\/video\/)([0-9]+)/,
twitch: /https?:\/\/(?:www.)?twitch.tv\/(?:(videos)\/)?([a-zA-Z0-9][\w]{3,24})/i,
......
......@@ -61,14 +61,14 @@ export class TagTransformingSanitizer {
const iframeToBeReturned: sanitize.Tag = {
tagName: 'iframe',
attribs: {
src,
width: this.options.iframeWidth + '',
height: this.options.iframeHeight + '',
// some of there are deprecated but required for some embeds
frameborder: '0',
allowfullscreen: 'allowfullscreen',
// deprecated but required for vimeo : https://vimeo.com/forums/help/topic:278181
webkitallowfullscreen: 'webkitallowfullscreen',
mozallowfullscreen: 'mozallowfullscreen', // deprecated but required for vimeo
src,
width: this.options.iframeWidth + '',
height: this.options.iframeHeight + ''
mozallowfullscreen: 'mozallowfullscreen'
}
};
return iframeToBeReturned;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment