Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added new bookmark card #11024

Merged
merged 14 commits into from Aug 27, 2019
Merged
143 changes: 99 additions & 44 deletions core/server/api/canary/oembed.js
Expand Up @@ -3,6 +3,44 @@ const {extract, hasProvider} = require('oembed-parser');
const Promise = require('bluebird');
const request = require('../../lib/request');
const cheerio = require('cheerio');
const metascraper = require('metascraper')([
require('metascraper-url')(),
require('metascraper-title')(),
require('metascraper-description')(),
require('metascraper-author')(),
require('metascraper-publisher')(),
require('metascraper-image')(),
require('metascraper-logo')(),
require('metascraper-logo-favicon')()
]);

async function fetchBookmarkData(url, html) {
if (!html) {
const response = await request(url, {
headers: {
'user-agent': 'Ghost(https://github.com/TryGhost/Ghost)'
}
});
html = response.body;
}
const scraperResponse = await metascraper({html, url});
const metadata = Object.assign({}, scraperResponse, {
thumbnail: scraperResponse.image,
icon: scraperResponse.logo
});
// We want to use standard naming for image and logo
delete metadata.image;
delete metadata.logo;

if (metadata.title && metadata.description) {
return Promise.resolve({
type: 'bookmark',
url,
metadata
});
}
return Promise.resolve();
}

const findUrlWithProvider = (url) => {
let provider;
Expand Down Expand Up @@ -32,65 +70,82 @@ const getOembedUrlFromHTML = (html) => {
return cheerio('link[type="application/json+oembed"]', html).attr('href');
};

function unknownProvider(url) {
return Promise.reject(new common.errors.ValidationError({
message: common.i18n.t('errors.api.oembed.unknownProvider'),
context: url
}));
}

function knownProvider(url) {
return extract(url).catch((err) => {
return Promise.reject(new common.errors.InternalServerError({
message: err.message
}));
});
}

function fetchOembedData(url) {
let provider;
({url, provider} = findUrlWithProvider(url));
if (provider) {
return knownProvider(url);
}
return request(url, {
method: 'GET',
timeout: 2 * 1000,
followRedirect: true,
headers: {
'user-agent': 'Ghost(https://github.com/TryGhost/Ghost)'
}
}).then((response) => {
if (response.url !== url) {
({url, provider} = findUrlWithProvider(response.url));
}
if (provider) {
return knownProvider(url);
}
const oembedUrl = getOembedUrlFromHTML(response.body);
if (oembedUrl) {
return request(oembedUrl, {
method: 'GET',
json: true
}).then((response) => {
return response.body;
}).catch(() => {});
}
});
}

module.exports = {
docName: 'oembed',

read: {
permissions: false,
data: [
'url'
'url',
'type'
],
options: [],
query({data}) {
let {url} = data;

function unknownProvider() {
return Promise.reject(new common.errors.ValidationError({
message: common.i18n.t('errors.api.oembed.unknownProvider'),
context: url
}));
}

function knownProvider(url) {
return extract(url).catch((err) => {
return Promise.reject(new common.errors.InternalServerError({
message: err.message
}));
});
}
let {url, type} = data;

let provider;
({url, provider} = findUrlWithProvider(url));

if (provider) {
return knownProvider(url);
if (type === 'bookmark') {
return fetchBookmarkData(url);
}

// see if the URL is a redirect to cater for shortened urls
return request(url, {
method: 'GET',
timeout: 2 * 1000,
followRedirect: true
}).then((response) => {
if (response.url !== url) {
({url, provider} = findUrlWithProvider(response.url));
return provider ? knownProvider(url) : unknownProvider();
return fetchOembedData(url).then((response) => {
if (!response && !type) {
return fetchBookmarkData(url);
}

const oembedUrl = getOembedUrlFromHTML(response.body);

if (!oembedUrl) {
return unknownProvider();
return response;
}).then((response) => {
if (!response) {
return unknownProvider(url);
}

return request(oembedUrl, {
method: 'GET',
json: true
}).then((response) => {
return response.body;
});
return response;
}).catch(() => {
return unknownProvider();
return unknownProvider(url);
});
}
}
Expand Down
93 changes: 93 additions & 0 deletions core/server/lib/mobiledoc/cards/bookmark.js
@@ -0,0 +1,93 @@
/**
<figure class="kg-card kg-bookmark-card">
<a href="[URL]" class="kg-bookmark-container">
<div class="kg-bookmark-content">
<div class="kg-bookmark-title">[TITLE]</div>
<div class="kg-bookmark-description">[DESCRIPTION]</div>
<div class="kg-bookmark-metadata">
<img src="[ICON]" class="kg-bookmark-icon">
<span class="kg-bookmark-author">[AUTHOR]</span>
<span class="kg-bookmark-publisher">[PUBLISHER]</span>
</div>
</div>
<div class="kg-bookmark-thumbnail">
<img src="[THUMBNAIL]">
</div>
</a>
</figure>
*/

const createCard = require('../create-card');

function createElement(dom, elem, classNames = '', attributes = [], text) {
let element = dom.createElement(elem);
if (classNames) {
element.setAttribute('class', classNames);
}
attributes.forEach((attr) => {
element.setAttribute(attr.key, attr.value);
});
if (text) {
element.appendChild(dom.createTextNode(text));
}
return element;
}

module.exports = createCard({
name: 'bookmark',
type: 'dom',
render(opts) {
if (!opts.payload.metadata) {
return '';
}

let {payload, env: {dom}} = opts;
let figure = createElement(dom, 'figure', 'kg-card kg-bookmark-card');
let linkTag = createElement(dom, 'a', 'kg-bookmark-container', [{
key: 'href',
value: payload.metadata.url
}]);
let contentDiv = createElement(dom, 'div', 'kg-bookmark-content');
let titleDiv = createElement(dom, 'div', 'kg-bookmark-title', [] , payload.metadata.title);
let descriptionDiv = createElement(dom, 'div', 'kg-bookmark-description', [] , payload.metadata.description);
let metadataDiv = createElement(dom, 'div', 'kg-bookmark-metadata');
let imgIcon = createElement(dom, 'img', 'kg-bookmark-icon', [{
key: 'src',
value: payload.metadata.icon
}]);
let authorSpan = createElement(dom, 'span', 'kg-bookmark-author', [] , payload.metadata.author);
let publisherSpan = createElement(dom, 'span', 'kg-bookmark-publisher', [] , payload.metadata.publisher);
let thumbnailDiv = createElement(dom, 'div', 'kg-bookmark-thumbnail');
let thumbnailImg = createElement(dom, 'img', '', [{
key: 'src',
value: payload.metadata.thumbnail
}]);
thumbnailDiv.appendChild(thumbnailImg);
if (payload.metadata.icon) {
metadataDiv.appendChild(imgIcon);
}
if (payload.metadata.author) {
metadataDiv.appendChild(authorSpan);
}
if (payload.metadata.publisher) {
metadataDiv.appendChild(publisherSpan);
}
contentDiv.appendChild(titleDiv);
contentDiv.appendChild(descriptionDiv);
contentDiv.appendChild(metadataDiv);
linkTag.appendChild(contentDiv);
if (payload.metadata.thumbnail) {
linkTag.appendChild(thumbnailDiv);
}
figure.appendChild(linkTag);

if (payload.caption) {
let figcaption = dom.createElement('figcaption');
figcaption.appendChild(dom.createRawHTMLSection(payload.caption));
figure.appendChild(figcaption);
figure.setAttribute('class', `${figure.getAttribute('class')} kg-card-hascaption`);
}

return figure;
}
});
1 change: 1 addition & 0 deletions core/server/lib/mobiledoc/cards/index.js
Expand Up @@ -2,6 +2,7 @@ module.exports = [
require('./card-markdown'),
require('./code'),
require('./embed'),
require('./bookmark'),
require('./hr'),
require('./html'),
require('./image'),
Expand Down
9 changes: 9 additions & 0 deletions package.json
Expand Up @@ -98,6 +98,15 @@
"markdown-it-footnote": "3.0.2",
"markdown-it-lazy-headers": "0.1.3",
"markdown-it-mark": "2.0.0",
"metascraper": "5.6.6",
"metascraper-url": "5.6.6",
"metascraper-title": "5.6.6",
"metascraper-description": "5.6.6",
"metascraper-author": "5.6.6",
"metascraper-publisher": "5.6.6",
"metascraper-image": "5.6.6",
"metascraper-logo": "5.6.6",
"metascraper-logo-favicon": "5.6.6",
"mobiledoc-dom-renderer": "0.6.6",
"moment": "2.24.0",
"moment-timezone": "0.5.23",
Expand Down