mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-23 02:41:50 +03:00
✨ Added Revue Importer (#16012)
refs: https://www.getrevue.co/app/offboard - Revue is stopping all paid subscriptions on 20th Dec, and shutting down on Jan 18th. - This update allows Ghost to accept and handle the zip file Revue are providing as an export in Labs > Importer - It will import posts (as best as we can with the data provided) and subscribers as free members - At present it doesn't import paid subscribers, as we don't have that info, but you can disconnect Revue from your Stripe account to prevent all your subscriptions being cancelled & there's the option this can be fixed later - There will be further updates to polish up this tooling - this is just a first pass to try to get something in people's hands Co-authored-by: Paul Davis <PaulAdamDavis@users.noreply.github.com>
This commit is contained in:
parent
0825a2d7f4
commit
5f90baf6fe
@ -2,6 +2,7 @@ const _ = require('lodash');
|
||||
const fs = require('fs-extra');
|
||||
const tpl = require('@tryghost/tpl');
|
||||
const errors = require('@tryghost/errors');
|
||||
const debug = require('@tryghost/debug')('importer:handler:data');
|
||||
|
||||
const messages = {
|
||||
invalidJsonFormat: 'Invalid JSON format, expected `{ db: [exportedData] }`',
|
||||
@ -17,6 +18,7 @@ JSONHandler = {
|
||||
directories: [],
|
||||
|
||||
loadFile: async function (files, startDir) { // eslint-disable-line no-unused-vars
|
||||
debug('loadFile', files);
|
||||
// @TODO: Handle multiple JSON files
|
||||
const filePath = files[0].path;
|
||||
|
||||
|
44
ghost/core/core/server/data/importer/handlers/revue.js
Normal file
44
ghost/core/core/server/data/importer/handlers/revue.js
Normal file
@ -0,0 +1,44 @@
|
||||
const _ = require('lodash');
|
||||
const fs = require('fs-extra');
|
||||
const debug = require('@tryghost/debug')('importer:handler:revue');
|
||||
|
||||
const hasIssuesCSV = (files) => {
|
||||
return _.some(files, (file) => {
|
||||
return file.name.match(/^issues.*?\.csv/);
|
||||
});
|
||||
};
|
||||
|
||||
const RevueHandler = {
|
||||
type: 'revue',
|
||||
extensions: ['.csv', '.json'],
|
||||
contentTypes: ['application/octet-stream', 'application/json', 'text/plain'],
|
||||
directories: [],
|
||||
|
||||
loadFile: function (files, startDir) {
|
||||
debug('loadFile', files);
|
||||
const startDirRegex = startDir ? new RegExp('^' + startDir + '/') : new RegExp('');
|
||||
const idRegex = /_.*?\./;
|
||||
const ops = [];
|
||||
const revue = {};
|
||||
|
||||
if (!hasIssuesCSV(files)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
_.each(files, function (file) {
|
||||
ops.push(fs.readFile(file.path).then(function (content) {
|
||||
// normalize the file name
|
||||
file.name = file.name.replace(startDirRegex, '').replace(idRegex, '.');
|
||||
const name = file.name.split('.')[0];
|
||||
|
||||
revue[name] = content.toString();
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(ops).then(() => {
|
||||
return {meta: {revue: true}, revue};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = RevueHandler;
|
@ -7,12 +7,15 @@ const uuid = require('uuid');
|
||||
const config = require('../../../shared/config');
|
||||
const {extract} = require('@tryghost/zip');
|
||||
const tpl = require('@tryghost/tpl');
|
||||
const debug = require('@tryghost/debug')('import-manager');
|
||||
const logging = require('@tryghost/logging');
|
||||
const errors = require('@tryghost/errors');
|
||||
const ImageHandler = require('./handlers/image');
|
||||
const RevueHandler = require('./handlers/revue');
|
||||
const JSONHandler = require('./handlers/json');
|
||||
const MarkdownHandler = require('./handlers/markdown');
|
||||
const ImageImporter = require('./importers/image');
|
||||
const RevueImporter = require('@tryghost/importer-revue');
|
||||
const DataImporter = require('./importers/data');
|
||||
const urlUtils = require('../../../shared/url-utils');
|
||||
const {GhostMailer} = require('../../services/mail');
|
||||
@ -48,12 +51,12 @@ class ImportManager {
|
||||
/**
|
||||
* @type {Importer[]} importers
|
||||
*/
|
||||
this.importers = [ImageImporter, DataImporter];
|
||||
this.importers = [ImageImporter, RevueImporter, DataImporter];
|
||||
|
||||
/**
|
||||
* @type {Handler[]}
|
||||
*/
|
||||
this.handlers = [ImageHandler, JSONHandler, MarkdownHandler];
|
||||
this.handlers = [ImageHandler, RevueHandler, JSONHandler, MarkdownHandler];
|
||||
|
||||
// Keep track of file to cleanup at the end
|
||||
/**
|
||||
@ -240,6 +243,8 @@ class ImportManager {
|
||||
for (const handler of this.handlers) {
|
||||
const files = this.getFilesFromZip(handler, zipDirectory);
|
||||
|
||||
debug('handler', handler.type, files);
|
||||
|
||||
if (files.length > 0) {
|
||||
if (Object.prototype.hasOwnProperty.call(importData, handler.type)) {
|
||||
// This limitation is here to reduce the complexity of the importer for now
|
||||
@ -271,17 +276,19 @@ class ImportManager {
|
||||
* @param {File} file
|
||||
* @returns {Promise<ImportData>}
|
||||
*/
|
||||
processFile(file, ext) {
|
||||
const fileHandler = _.find(this.handlers, function (handler) {
|
||||
async processFile(file, ext) {
|
||||
const fileHandlers = _.filter(this.handlers, function (handler) {
|
||||
return _.includes(handler.extensions, ext);
|
||||
});
|
||||
|
||||
return fileHandler.loadFile([_.pick(file, 'name', 'path')]).then(function (loadedData) {
|
||||
// normalize the returned data
|
||||
const importData = {};
|
||||
importData[fileHandler.type] = loadedData;
|
||||
|
||||
await Promise.all(fileHandlers.map(async (fileHandler) => {
|
||||
debug('fileHandler', fileHandler.type);
|
||||
importData[fileHandler.type] = await fileHandler.loadFile([_.pick(file, 'name', 'path')]);
|
||||
}));
|
||||
|
||||
return importData;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -305,6 +312,7 @@ class ImportManager {
|
||||
* @returns {Promise<ImportData>}
|
||||
*/
|
||||
async preProcess(importData) {
|
||||
debug('preProcess');
|
||||
for (const importer of this.importers) {
|
||||
importData = importer.preProcess(importData);
|
||||
}
|
||||
@ -321,10 +329,12 @@ class ImportManager {
|
||||
* @returns {Promise<Object.<string, ImportResult>>} importResults
|
||||
*/
|
||||
async doImport(importData, importOptions) {
|
||||
debug('doImport', this.importers);
|
||||
importOptions = importOptions || {};
|
||||
const importResults = {};
|
||||
|
||||
for (const importer of this.importers) {
|
||||
debug('importer looking for', importer.type, 'in', Object.keys(importData));
|
||||
if (Object.prototype.hasOwnProperty.call(importData, importer.type)) {
|
||||
importResults[importer.type] = await importer.doImport(importData[importer.type], importOptions);
|
||||
}
|
||||
@ -411,6 +421,8 @@ class ImportManager {
|
||||
importData = await this.loadFile(file);
|
||||
}
|
||||
|
||||
debug('importFromFile completed file load', importData);
|
||||
|
||||
const env = config.get('env');
|
||||
if (!env?.startsWith('testing') && !importOptions.runningInJob) {
|
||||
return jobManager.addJob({
|
||||
|
@ -14,6 +14,7 @@ const ProductsImporter = require('./products');
|
||||
const StripeProductsImporter = require('./stripe-products');
|
||||
const StripePricesImporter = require('./stripe-prices');
|
||||
const CustomThemeSettingsImporter = require('./custom-theme-settings');
|
||||
const RevueSubscriberImporter = require('./revue-subscriber');
|
||||
const RolesImporter = require('./roles');
|
||||
const {slugify} = require('@tryghost/string/lib');
|
||||
|
||||
@ -24,6 +25,7 @@ DataImporter = {
|
||||
type: 'data',
|
||||
|
||||
preProcess: function preProcess(importData) {
|
||||
debug('preProcess');
|
||||
importData.preProcessedByData = true;
|
||||
return importData;
|
||||
},
|
||||
@ -39,12 +41,14 @@ DataImporter = {
|
||||
importers.stripe_prices = new StripePricesImporter(importData.data);
|
||||
importers.posts = new PostsImporter(importData.data);
|
||||
importers.custom_theme_settings = new CustomThemeSettingsImporter(importData.data);
|
||||
importers.revue_subscribers = new RevueSubscriberImporter(importData.data);
|
||||
|
||||
return importData;
|
||||
},
|
||||
|
||||
// Allow importing with an options object that is passed through the importer
|
||||
doImport: async function doImport(importData, importOptions) {
|
||||
debug('doImport');
|
||||
importOptions = importOptions || {};
|
||||
|
||||
if (importOptions.importTag && importData?.data?.posts) {
|
||||
|
@ -0,0 +1,24 @@
|
||||
const debug = require('@tryghost/debug')('importer:revue-subscriber');
|
||||
const BaseImporter = require('./base');
|
||||
|
||||
class RevueSubscriberImporter extends BaseImporter {
|
||||
constructor(allDataFromFile) {
|
||||
super(allDataFromFile, {
|
||||
modelName: 'Member',
|
||||
dataKeyToImport: 'revue_subscribers'
|
||||
});
|
||||
}
|
||||
|
||||
beforeImport() {
|
||||
debug('beforeImport');
|
||||
return super.beforeImport();
|
||||
}
|
||||
|
||||
async doImport(options, importOptions) {
|
||||
debug('doImport', this.modelName, this.dataToImport.length);
|
||||
|
||||
return super.doImport(options, importOptions);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RevueSubscriberImporter;
|
@ -1,6 +1,8 @@
|
||||
const testUtils = require('../../utils');
|
||||
const importer = require('../../../core/server/data/importer');
|
||||
const dataImporter = importer.importers[1];
|
||||
const dataImporter = importer.importers.find((instance) => {
|
||||
return instance.type === 'data';
|
||||
});
|
||||
|
||||
const {exportedBodyLegacy} = require('../../utils/fixtures/export/body-generator');
|
||||
|
||||
|
@ -3,7 +3,9 @@ const {exportedBodyV1} = require('../../utils/fixtures/export/body-generator');
|
||||
|
||||
const models = require('../../../core/server/models');
|
||||
const importer = require('../../../core/server/data/importer');
|
||||
const dataImporter = importer.importers[1];
|
||||
const dataImporter = importer.importers.find((instance) => {
|
||||
return instance.type === 'data';
|
||||
});
|
||||
|
||||
const importOptions = {
|
||||
returnImportedData: true
|
||||
|
@ -13,7 +13,9 @@ const db = require('../../../core/server/data/db');
|
||||
|
||||
const models = require('../../../core/server/models');
|
||||
const importer = require('../../../core/server/data/importer');
|
||||
const dataImporter = importer.importers[1];
|
||||
const dataImporter = importer.importers.find((instance) => {
|
||||
return instance.type === 'data';
|
||||
});
|
||||
|
||||
const importOptions = {
|
||||
returnImportedData: true
|
||||
|
@ -14,8 +14,10 @@ const ImportManager = require('../../../../../core/server/data/importer');
|
||||
const JSONHandler = require('../../../../../core/server/data/importer/handlers/json');
|
||||
let ImageHandler = rewire('../../../../../core/server/data/importer/handlers/image');
|
||||
const MarkdownHandler = require('../../../../../core/server/data/importer/handlers/markdown');
|
||||
const RevueHandler = require('../../../../../core/server/data/importer/handlers/revue');
|
||||
const DataImporter = require('../../../../../core/server/data/importer/importers/data');
|
||||
const ImageImporter = require('../../../../../core/server/data/importer/importers/image');
|
||||
const RevueImporter = require('@tryghost/importer-revue');
|
||||
const storage = require('../../../../../core/server/adapters/storage');
|
||||
const configUtils = require('../../../../utils/configUtils');
|
||||
|
||||
@ -28,8 +30,8 @@ describe('Importer', function () {
|
||||
|
||||
describe('ImportManager', function () {
|
||||
it('has the correct interface', function () {
|
||||
ImportManager.handlers.should.be.instanceof(Array).and.have.lengthOf(3);
|
||||
ImportManager.importers.should.be.instanceof(Array).and.have.lengthOf(2);
|
||||
ImportManager.handlers.should.be.instanceof(Array).and.have.lengthOf(4);
|
||||
ImportManager.importers.should.be.instanceof(Array).and.have.lengthOf(3);
|
||||
ImportManager.loadFile.should.be.instanceof(Function);
|
||||
ImportManager.preProcess.should.be.instanceof(Function);
|
||||
ImportManager.doImport.should.be.instanceof(Function);
|
||||
@ -37,7 +39,8 @@ describe('Importer', function () {
|
||||
});
|
||||
|
||||
it('gets the correct extensions', function () {
|
||||
ImportManager.getExtensions().should.be.instanceof(Array).and.have.lengthOf(12);
|
||||
ImportManager.getExtensions().should.be.instanceof(Array).and.have.lengthOf(13);
|
||||
ImportManager.getExtensions().should.containEql('.csv');
|
||||
ImportManager.getExtensions().should.containEql('.json');
|
||||
ImportManager.getExtensions().should.containEql('.zip');
|
||||
ImportManager.getExtensions().should.containEql('.jpg');
|
||||
@ -72,7 +75,7 @@ describe('Importer', function () {
|
||||
|
||||
it('globs extensions correctly', function () {
|
||||
ImportManager.getGlobPattern(ImportManager.getExtensions())
|
||||
.should.equal('+(.jpg|.jpeg|.gif|.png|.svg|.svgz|.ico|.webp|.json|.md|.markdown|.zip)');
|
||||
.should.equal('+(.jpg|.jpeg|.gif|.png|.svg|.svgz|.ico|.webp|.csv|.json|.md|.markdown|.zip)');
|
||||
ImportManager.getGlobPattern(ImportManager.getDirectories())
|
||||
.should.equal('+(images|content)');
|
||||
ImportManager.getGlobPattern(JSONHandler.extensions)
|
||||
@ -80,19 +83,19 @@ describe('Importer', function () {
|
||||
ImportManager.getGlobPattern(ImageHandler.extensions)
|
||||
.should.equal('+(.jpg|.jpeg|.gif|.png|.svg|.svgz|.ico|.webp)');
|
||||
ImportManager.getExtensionGlob(ImportManager.getExtensions())
|
||||
.should.equal('*+(.jpg|.jpeg|.gif|.png|.svg|.svgz|.ico|.webp|.json|.md|.markdown|.zip)');
|
||||
.should.equal('*+(.jpg|.jpeg|.gif|.png|.svg|.svgz|.ico|.webp|.csv|.json|.md|.markdown|.zip)');
|
||||
ImportManager.getDirectoryGlob(ImportManager.getDirectories())
|
||||
.should.equal('+(images|content)');
|
||||
ImportManager.getExtensionGlob(ImportManager.getExtensions(), 0)
|
||||
.should.equal('*+(.jpg|.jpeg|.gif|.png|.svg|.svgz|.ico|.webp|.json|.md|.markdown|.zip)');
|
||||
.should.equal('*+(.jpg|.jpeg|.gif|.png|.svg|.svgz|.ico|.webp|.csv|.json|.md|.markdown|.zip)');
|
||||
ImportManager.getDirectoryGlob(ImportManager.getDirectories(), 0)
|
||||
.should.equal('+(images|content)');
|
||||
ImportManager.getExtensionGlob(ImportManager.getExtensions(), 1)
|
||||
.should.equal('{*/*,*}+(.jpg|.jpeg|.gif|.png|.svg|.svgz|.ico|.webp|.json|.md|.markdown|.zip)');
|
||||
.should.equal('{*/*,*}+(.jpg|.jpeg|.gif|.png|.svg|.svgz|.ico|.webp|.csv|.json|.md|.markdown|.zip)');
|
||||
ImportManager.getDirectoryGlob(ImportManager.getDirectories(), 1)
|
||||
.should.equal('{*/,}+(images|content)');
|
||||
ImportManager.getExtensionGlob(ImportManager.getExtensions(), 2)
|
||||
.should.equal('**/*+(.jpg|.jpeg|.gif|.png|.svg|.svgz|.ico|.webp|.json|.md|.markdown|.zip)');
|
||||
.should.equal('**/*+(.jpg|.jpeg|.gif|.png|.svg|.svgz|.ico|.webp|.csv|.json|.md|.markdown|.zip)');
|
||||
ImportManager.getDirectoryGlob(ImportManager.getDirectories(), 2)
|
||||
.should.equal('**/+(images|content)');
|
||||
});
|
||||
@ -142,8 +145,8 @@ describe('Importer', function () {
|
||||
// We need to make sure we don't actually extract a zip and leave temporary files everywhere!
|
||||
it('knows when to process a zip', function (done) {
|
||||
const testZip = {name: 'myFile.zip', path: '/my/path/myFile.zip'};
|
||||
const zipSpy = sinon.stub(ImportManager, 'processZip').returns(Promise.resolve({}));
|
||||
const fileSpy = sinon.stub(ImportManager, 'processFile').returns(Promise.resolve({}));
|
||||
const zipSpy = sinon.stub(ImportManager, 'processZip').resolves({});
|
||||
const fileSpy = sinon.stub(ImportManager, 'processFile').resolves({});
|
||||
|
||||
ImportManager.loadFile(testZip).then(function () {
|
||||
zipSpy.calledOnce.should.be.true();
|
||||
@ -157,26 +160,29 @@ describe('Importer', function () {
|
||||
const testZip = {name: 'myFile.zip', path: '/my/path/myFile.zip'};
|
||||
|
||||
// need to stub out the extract and glob function for zip
|
||||
const extractSpy = sinon.stub(ImportManager, 'extractZip').returns(Promise.resolve('/tmp/dir/'));
|
||||
const extractSpy = sinon.stub(ImportManager, 'extractZip').resolves('/tmp/dir/');
|
||||
|
||||
const validSpy = sinon.stub(ImportManager, 'isValidZip').returns(true);
|
||||
const baseDirSpy = sinon.stub(ImportManager, 'getBaseDirectory').returns('');
|
||||
const getFileSpy = sinon.stub(ImportManager, 'getFilesFromZip');
|
||||
const jsonSpy = sinon.stub(JSONHandler, 'loadFile').returns(Promise.resolve({posts: []}));
|
||||
const revueSpy = sinon.stub(RevueHandler, 'loadFile').resolves();
|
||||
const jsonSpy = sinon.stub(JSONHandler, 'loadFile').resolves({posts: []});
|
||||
const imageSpy = sinon.stub(ImageHandler, 'loadFile');
|
||||
const mdSpy = sinon.stub(MarkdownHandler, 'loadFile');
|
||||
|
||||
getFileSpy.returns([]);
|
||||
getFileSpy.withArgs(JSONHandler, sinon.match.string).returns([{path: '/tmp/dir/myFile.json', name: 'myFile.json'}]);
|
||||
getFileSpy.withArgs(RevueHandler, sinon.match.string).returns([{path: '/tmp/dir/myFile.json', name: 'myFile.json'}]);
|
||||
|
||||
ImportManager.processZip(testZip).then(function (zipResult) {
|
||||
extractSpy.calledOnce.should.be.true();
|
||||
validSpy.calledOnce.should.be.true();
|
||||
baseDirSpy.calledOnce.should.be.true();
|
||||
getFileSpy.calledThrice.should.be.true();
|
||||
getFileSpy.callCount.should.eql(4);
|
||||
jsonSpy.calledOnce.should.be.true();
|
||||
imageSpy.called.should.be.false();
|
||||
mdSpy.called.should.be.false();
|
||||
revueSpy.called.should.be.true();
|
||||
|
||||
ImportManager.processFile(testFile, '.json').then(function (fileResult) {
|
||||
jsonSpy.calledTwice.should.be.true();
|
||||
@ -234,6 +240,7 @@ describe('Importer', function () {
|
||||
this.beforeEach(() => {
|
||||
sinon.stub(JSONHandler, 'loadFile').returns(Promise.resolve({posts: []}));
|
||||
sinon.stub(ImageHandler, 'loadFile');
|
||||
sinon.stub(RevueHandler, 'loadFile');
|
||||
sinon.stub(MarkdownHandler, 'loadFile');
|
||||
});
|
||||
|
||||
@ -353,8 +360,11 @@ describe('Importer', function () {
|
||||
|
||||
const dataSpy = sinon.spy(DataImporter, 'preProcess');
|
||||
const imageSpy = sinon.spy(ImageImporter, 'preProcess');
|
||||
const revueSpy = sinon.spy(RevueImporter, 'preProcess');
|
||||
|
||||
ImportManager.preProcess(inputCopy).then(function (output) {
|
||||
revueSpy.calledOnce.should.be.true();
|
||||
revueSpy.calledWith(inputCopy).should.be.true();
|
||||
dataSpy.calledOnce.should.be.true();
|
||||
dataSpy.calledWith(inputCopy).should.be.true();
|
||||
imageSpy.calledOnce.should.be.true();
|
||||
|
6
ghost/importer-revue/.eslintrc.js
Normal file
6
ghost/importer-revue/.eslintrc.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: ['ghost'],
|
||||
extends: [
|
||||
'plugin:ghost/node'
|
||||
]
|
||||
};
|
22
ghost/importer-revue/README.md
Normal file
22
ghost/importer-revue/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
# Importer Revue
|
||||
|
||||
Revue importer
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
## Develop
|
||||
|
||||
This is a monorepo package.
|
||||
|
||||
Follow the instructions for the top-level repo.
|
||||
1. `git clone` this repo & `cd` into it as usual
|
||||
2. Run `yarn` to install top-level dependencies.
|
||||
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
- `yarn lint` run just eslint
|
||||
- `yarn test` run lint and tests
|
1
ghost/importer-revue/index.js
Normal file
1
ghost/importer-revue/index.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('./lib/importer-revue');
|
129
ghost/importer-revue/lib/importer-revue.js
Normal file
129
ghost/importer-revue/lib/importer-revue.js
Normal file
@ -0,0 +1,129 @@
|
||||
const debug = require('@tryghost/debug')('importer:revue');
|
||||
const {slugify} = require('@tryghost/string');
|
||||
const papaparse = require('papaparse');
|
||||
const _ = require('lodash');
|
||||
|
||||
const JSONToHTML = require('../lib/json-to-html');
|
||||
|
||||
/**
|
||||
* Build posts out of the issue and item data
|
||||
*
|
||||
* @param {Object} revueData
|
||||
* @return {Array}
|
||||
*/
|
||||
const fetchPostsFromData = (revueData) => {
|
||||
const itemData = JSON.parse(revueData.items);
|
||||
const issueData = papaparse.parse(revueData.issues, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
transform(value, header) {
|
||||
if (header === 'id') {
|
||||
return parseInt(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
});
|
||||
|
||||
const posts = [];
|
||||
|
||||
issueData.data.forEach((postMeta) => {
|
||||
// Convert issues to posts
|
||||
if (!postMeta.subject) {
|
||||
return;
|
||||
}
|
||||
|
||||
const revuePostID = postMeta.id;
|
||||
let postHTML = postMeta.description;
|
||||
|
||||
const postItems = _.filter(itemData, {issue_id: revuePostID});
|
||||
const sortedPostItems = _.sortBy(postItems, o => o.order);
|
||||
|
||||
if (postItems) {
|
||||
const convertedItems = JSONToHTML.itemsToHtml(sortedPostItems);
|
||||
postHTML = `${postMeta.description}${convertedItems}`;
|
||||
}
|
||||
|
||||
const postDate = JSONToHTML.getPostDate(postMeta);
|
||||
|
||||
posts.push({
|
||||
comment_id: revuePostID,
|
||||
title: postMeta.subject,
|
||||
slug: slugify(postMeta.subject),
|
||||
status: JSONToHTML.getPostStatus(postMeta),
|
||||
visibility: 'public',
|
||||
created_at: postDate,
|
||||
published_at: postDate,
|
||||
updated_at: postDate,
|
||||
html: postHTML,
|
||||
tags: ['#revue']
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
return posts;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} revueData
|
||||
*/
|
||||
const buildSubscriberList = (revueData) => {
|
||||
const subscribers = [];
|
||||
|
||||
const subscriberData = papaparse.parse(revueData.subscribers, {
|
||||
header: true,
|
||||
skipEmptyLines: true
|
||||
});
|
||||
|
||||
subscriberData.data.forEach((subscriber) => {
|
||||
subscribers.push({
|
||||
email: subscriber.email,
|
||||
name: `${subscriber.first_name} ${subscriber.last_name}`.trim(),
|
||||
created_at: subscriber.created_at
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
return subscribers;
|
||||
};
|
||||
|
||||
const RevueImporter = {
|
||||
type: 'revue',
|
||||
preProcess: function (importData) {
|
||||
debug('preProcess');
|
||||
importData.preProcessedByRevue = true;
|
||||
|
||||
// TODO: this should really be in doImport
|
||||
// No posts to prePprocess, return early
|
||||
if (!importData?.revue?.revue?.issues) {
|
||||
return importData;
|
||||
}
|
||||
|
||||
// This processed data goes to the data importer
|
||||
importData.data = {
|
||||
meta: {version: '5.0.0'},
|
||||
data: {}
|
||||
};
|
||||
|
||||
importData.data.data.posts = this.importPosts(importData.revue.revue);
|
||||
|
||||
// No subscribers to import, we're done
|
||||
if (!importData?.revue?.revue?.subscribers) {
|
||||
return importData;
|
||||
}
|
||||
|
||||
importData.data.data.revue_subscribers = this.importSubscribers(importData.revue.revue);
|
||||
|
||||
return importData;
|
||||
},
|
||||
doImport: function (importData) {
|
||||
debug('doImport');
|
||||
|
||||
return importData;
|
||||
},
|
||||
|
||||
importPosts: fetchPostsFromData,
|
||||
importSubscribers: buildSubscriberList
|
||||
};
|
||||
|
||||
module.exports = RevueImporter;
|
95
ghost/importer-revue/lib/json-to-html.js
Normal file
95
ghost/importer-revue/lib/json-to-html.js
Normal file
@ -0,0 +1,95 @@
|
||||
const SimpleDom = require('simple-dom');
|
||||
const serializer = new SimpleDom.HTMLSerializer(SimpleDom.voidMap);
|
||||
const imageCard = require('@tryghost/kg-default-cards/lib/cards/image.js');
|
||||
const embedCard = require('@tryghost/kg-default-cards/lib/cards/embed.js');
|
||||
|
||||
// Take the array of items for a specific post and return the converted HTML
|
||||
const itemsToHtml = (items) => {
|
||||
let itemHTMLChunks = [];
|
||||
items.forEach((item) => {
|
||||
let type = item.item_type;
|
||||
|
||||
if (type === 'header') {
|
||||
itemHTMLChunks.push(`<h3>${item.title}</h3>`);
|
||||
} else if (type === 'text') {
|
||||
itemHTMLChunks.push(item.description); // THis is basic text HTML with <p>, <b>, <a>, etc (no media)
|
||||
} else if (type === 'image') {
|
||||
// We have 2 values to work with here. `image` is smaller and most suitable, and `original_image_url` is the full-res that would need to be resized
|
||||
// - item.image (https://s3.amazonaws.com/revue/items/images/019/005/542/web/anita-austvika-C-JUrfmYqcw-unsplash.jpg?1667924147)
|
||||
// - item.original_image_url (https://s3.amazonaws.com/revue/items/images/019/005/542/original/anita-austvika-C-JUrfmYqcw-unsplash.jpg?1667924147)
|
||||
let cardOpts = {
|
||||
env: {dom: new SimpleDom.Document()},
|
||||
payload: {
|
||||
src: item.image,
|
||||
caption: item.description
|
||||
}
|
||||
};
|
||||
|
||||
itemHTMLChunks.push(serializer.serialize(imageCard.render(cardOpts)));
|
||||
} else if (type === 'link') {
|
||||
// This could be a bookmark, or it could be a paragraph of text with a linked header, there's no way to tell
|
||||
// The safest option here is to output an image with text under it
|
||||
let cardOpts = {
|
||||
env: {dom: new SimpleDom.Document()},
|
||||
payload: {
|
||||
src: item.image,
|
||||
caption: item.title,
|
||||
href: item.url
|
||||
}
|
||||
};
|
||||
itemHTMLChunks.push(serializer.serialize(imageCard.render(cardOpts)));
|
||||
|
||||
let linkHTML = `<h4><a href="${item.url}">${item.title}</a></h4>${item.description}`;
|
||||
itemHTMLChunks.push(linkHTML);
|
||||
} else if (type === 'tweet') {
|
||||
// Should this be an oEmbed call? Probably.
|
||||
itemHTMLChunks.push(`<figure class="kg-card kg-embed-card">
|
||||
<blockquote class="twitter-tweet"><a href="${item.url}"></a></blockquote>
|
||||
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
|
||||
</figure>`);
|
||||
} else if (type === 'video') {
|
||||
const isLongYouTube = /youtube.com/.test(item.url);
|
||||
const isShortYouTube = /youtu.be/.test(item.url);
|
||||
const isVimeo = /vimeo.com/.test(item.url);
|
||||
let videoHTML = '';
|
||||
if (isLongYouTube) {
|
||||
let videoID = item.url.replace(/https?:\/\/(?:www\.)?youtube\.com\/watch\?v=([a-zA-Z0-9_-]*)/gi, '$1');
|
||||
videoHTML = `<iframe width="200" height="113" src="https://www.youtube.com/embed/${videoID}?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`;
|
||||
} else if (isShortYouTube) {
|
||||
let videoID = item.url.replace(/https?:\/\/(?:www\.)?youtu\.be\/([a-zA-Z0-9_-]*)/gi, '$1');
|
||||
videoHTML = `<iframe width="200" height="113" src="https://www.youtube.com/embed/${videoID}?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`;
|
||||
} else if (isVimeo) {
|
||||
let videoID = item.url.replace(/https?:\/\/(?:www\.)?vimeo\.com\/([0-9]+)/gi, '$1');
|
||||
videoHTML = `<iframe src="https://player.vimeo.com/video/${videoID}" width="200" height="113" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>`;
|
||||
}
|
||||
let cardOpts = {
|
||||
env: {dom: new SimpleDom.Document()},
|
||||
payload: {
|
||||
html: videoHTML,
|
||||
caption: item.description
|
||||
}
|
||||
};
|
||||
|
||||
itemHTMLChunks.push(serializer.serialize(embedCard.render(cardOpts)));
|
||||
}
|
||||
});
|
||||
return itemHTMLChunks.join('\n');
|
||||
};
|
||||
|
||||
const getPostDate = (data) => {
|
||||
const isPublished = (data.sent_at) ? true : false; // This is how we determine is a post is published or not
|
||||
const postDate = (isPublished) ? new Date(data.sent_at) : new Date();
|
||||
|
||||
return postDate.toISOString();
|
||||
};
|
||||
|
||||
const getPostStatus = (data) => {
|
||||
const isPublished = (data.sent_at) ? true : false; // This is how we determine is a post is published or not
|
||||
return (isPublished) ? 'published' : 'draft';
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
itemsToHtml,
|
||||
getPostDate,
|
||||
getPostStatus
|
||||
};
|
31
ghost/importer-revue/package.json
Normal file
31
ghost/importer-revue/package.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@tryghost/importer-revue",
|
||||
"version": "0.0.0",
|
||||
"repository": "https://github.com/TryGhost/Ghost/tree/main/packages/importer-revue",
|
||||
"author": "Ghost Foundation",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "echo \"Implement me!\"",
|
||||
"test:unit": "NODE_ENV=testing c8 --all --check-coverage --100 --reporter text --reporter html --reporter cobertura mocha './test/**/*.test.js'",
|
||||
"test": "yarn test:unit",
|
||||
"lint:code": "eslint *.js lib/ --ext .js --cache",
|
||||
"lint": "yarn lint:code && yarn lint:test",
|
||||
"lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"lib"
|
||||
],
|
||||
"devDependencies": {
|
||||
"sinon": "15.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tryghost/debug": "^0.1.20",
|
||||
"@tryghost/kg-default-cards": "^5.18.7",
|
||||
"@tryghost/string": "0.2.2",
|
||||
"lodash": "^4.17.21",
|
||||
"papaparse": "^5.3.2",
|
||||
"simple-dom": "^1.4.0"
|
||||
}
|
||||
}
|
6
ghost/importer-revue/test/.eslintrc.js
Normal file
6
ghost/importer-revue/test/.eslintrc.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: ['ghost'],
|
||||
extends: [
|
||||
'plugin:ghost/test'
|
||||
]
|
||||
};
|
227
ghost/importer-revue/test/importer-revue.test.js
Normal file
227
ghost/importer-revue/test/importer-revue.test.js
Normal file
@ -0,0 +1,227 @@
|
||||
const assert = require('assert');
|
||||
const sinon = require('sinon');
|
||||
const RevueImporter = require('../index');
|
||||
|
||||
const JSONToHTML = require('../lib/json-to-html');
|
||||
|
||||
describe('Revue Importer', function () {
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe('preProcess', function () {
|
||||
it('marks any object as processed', function () {
|
||||
let result;
|
||||
|
||||
result = RevueImporter.preProcess({});
|
||||
assert.deepEqual(result.preProcessedByRevue, true, 'marks the object as processed');
|
||||
|
||||
result = RevueImporter.preProcess({revue: {}});
|
||||
assert.deepEqual(result.preProcessedByRevue, true, 'marks the object as processed');
|
||||
|
||||
result = RevueImporter.preProcess({data: {}});
|
||||
assert.deepEqual(result.preProcessedByRevue, true, 'marks the object as processed');
|
||||
});
|
||||
|
||||
it('ignores empty revue object', function () {
|
||||
const result = RevueImporter.preProcess({revue: {}});
|
||||
assert.deepEqual(result.revue, {}, 'revue object left empty');
|
||||
assert.deepEqual(result.data, undefined, 'no data object set');
|
||||
});
|
||||
|
||||
it('ignores empty nested revue object', function () {
|
||||
const result = RevueImporter.preProcess({revue: {revue: {}}});
|
||||
assert.deepEqual(result.revue.revue, {}, 'revue object left empty');
|
||||
assert.deepEqual(result.data, undefined, 'no data object set');
|
||||
});
|
||||
|
||||
it('handles revue with issue and item data', function () {
|
||||
const result = RevueImporter.preProcess({revue: {revue: {issues: 'id', items: '{}'}}});
|
||||
assert.deepEqual(result.revue.revue, {issues: 'id', items: '{}'}, 'revue object left as-is');
|
||||
assert.deepEqual(result.data, {meta: {version: '5.0.0'}, data: {posts: []}}, 'data object is set');
|
||||
});
|
||||
|
||||
it('handles revue with issue, item and subscribers data', function () {
|
||||
const result = RevueImporter.preProcess({revue: {revue: {issues: 'id', items: '{}', subscribers: 'email'}}});
|
||||
assert.deepEqual(result.revue.revue, {issues: 'id', items: '{}', subscribers: 'email'}, 'revue object left as-is');
|
||||
assert.deepEqual(result.data, {meta: {version: '5.0.0'}, data: {posts: [], revue_subscribers: []}}, 'data object is set');
|
||||
});
|
||||
});
|
||||
|
||||
describe('doImport', function () {
|
||||
it('does nothing', function () {
|
||||
const result = RevueImporter.doImport({x: {y: 'z'}});
|
||||
assert.deepEqual(result, {x: {y: 'z'}}, 'is just a pass-through');
|
||||
});
|
||||
});
|
||||
|
||||
describe('importPosts', function () {
|
||||
it('can process a published post without items', function () {
|
||||
const result = RevueImporter.importPosts({items: '[]', issues: 'id,description,sent_at,subject,preheader\n123456,"<p>Hello World!</p>",2022-12-01 01:01:30 UTC,Hello World - Issue #8,'});
|
||||
assert.deepEqual(result, [
|
||||
{
|
||||
comment_id: 123456,
|
||||
title: 'Hello World - Issue #8',
|
||||
slug: 'hello-world-issue-8',
|
||||
status: 'published',
|
||||
visibility: 'public',
|
||||
created_at: '2022-12-01T01:01:30.000Z',
|
||||
published_at: '2022-12-01T01:01:30.000Z',
|
||||
updated_at: '2022-12-01T01:01:30.000Z',
|
||||
html: '<p>Hello World!</p>',
|
||||
tags: ['#revue']
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('doesnt process a post with no subject', function () {
|
||||
const result = RevueImporter.importPosts({items: '[{"title":"","issue_id":123456,"item_type":"text","url":"","description":"\u003cp\u003eGoodbye World!\u003c/p\u003e","order":0}]', issues: 'id,description,sent_at,subject,preheader\n123456,"<p>Hello World!</p>",2022-12-01 01:01:30 UTC,,'});
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
|
||||
it('can process a published post with items', function () {
|
||||
const result = RevueImporter.importPosts({items: '[{"title":"","issue_id":123456,"item_type":"text","url":"","description":"\u003cp\u003eGoodbye World!\u003c/p\u003e","order":0}]', issues: 'id,description,sent_at,subject,preheader\n123456,"<p>Hello World!</p>",2022-12-01 01:01:30 UTC,Hello World - Issue #8,'});
|
||||
assert.deepEqual(result, [
|
||||
{
|
||||
comment_id: 123456,
|
||||
created_at: '2022-12-01T01:01:30.000Z',
|
||||
html: '<p>Hello World!</p><p>Goodbye World!</p>',
|
||||
published_at: '2022-12-01T01:01:30.000Z',
|
||||
status: 'published',
|
||||
tags: [
|
||||
'#revue'
|
||||
],
|
||||
title: 'Hello World - Issue #8',
|
||||
slug: 'hello-world-issue-8',
|
||||
updated_at: '2022-12-01T01:01:30.000Z',
|
||||
visibility: 'public'
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('can process a draft post with items', function () {
|
||||
sinon.stub(JSONToHTML, 'getPostDate').returns('2022-12-01T01:02:03.123Z');
|
||||
|
||||
const result = RevueImporter.importPosts({items: '[{"title":"","issue_id":123456,"item_type":"text","url":"","description":"\u003cp\u003eGoodbye World!\u003c/p\u003e","order":0}]', issues: 'id,description,sent_at,subject,preheader\n123456,"<p>Hello World!</p>",,Hello World - Issue #8,'});
|
||||
assert.deepEqual(result, [
|
||||
{
|
||||
comment_id: 123456,
|
||||
title: 'Hello World - Issue #8',
|
||||
slug: 'hello-world-issue-8',
|
||||
status: 'draft',
|
||||
visibility: 'public',
|
||||
created_at: '2022-12-01T01:02:03.123Z',
|
||||
published_at: '2022-12-01T01:02:03.123Z',
|
||||
updated_at: '2022-12-01T01:02:03.123Z',
|
||||
html: '<p>Hello World!</p><p>Goodbye World!</p>',
|
||||
tags: ['#revue']
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('importSubscribers', function () {
|
||||
it('can process a subscriber with only first name', function () {
|
||||
const result = RevueImporter.importSubscribers({subscribers: 'email,first_name,last_name,created_at\njoe@bloggs.me,Joe,"",2022-12-01 01:02:03.123457'});
|
||||
|
||||
assert.deepEqual(result, [{email: 'joe@bloggs.me', name: 'Joe', created_at: '2022-12-01 01:02:03.123457'}]);
|
||||
});
|
||||
|
||||
it('can process a subscriber with first and last name', function () {
|
||||
const result = RevueImporter.importSubscribers({subscribers: 'email,first_name,last_name,created_at\njoe@bloggs.me,Joe,Bloggs,2022-12-01 01:02:03.123457'});
|
||||
|
||||
assert.deepEqual(result, [{email: 'joe@bloggs.me', name: 'Joe Bloggs', created_at: '2022-12-01 01:02:03.123457'}]);
|
||||
});
|
||||
|
||||
it('can process multiple subscribers', function () {
|
||||
const result = RevueImporter.importSubscribers({subscribers: 'email,first_name,last_name,created_at\njoe@bloggs.me,Joe,Bloggs,2022-12-01 01:02:03.123457\njo@bloggs.me,Jo,Bloggs,2022-12-01 01:02:04.123457'});
|
||||
|
||||
assert.deepEqual(result, [{email: 'joe@bloggs.me', name: 'Joe Bloggs', created_at: '2022-12-01 01:02:03.123457'},{email: 'jo@bloggs.me', name: 'Jo Bloggs', created_at: '2022-12-01 01:02:04.123457'}]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('JSONToHTML helpers', function () {
|
||||
describe('getPostData', function () {
|
||||
it('can get date for published post', function () {
|
||||
const result = JSONToHTML.getPostDate({sent_at: '2022-12-01 01:01:30 UTC'});
|
||||
|
||||
assert.deepEqual(result, '2022-12-01T01:01:30.000Z');
|
||||
});
|
||||
|
||||
it('can get date for draft post', function () {
|
||||
const result = JSONToHTML.getPostDate({});
|
||||
|
||||
assert.equal(result, new Date().toISOString());
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPostStatus', function () {
|
||||
it('can get date for published post', function () {
|
||||
const result = JSONToHTML.getPostStatus({sent_at: '2022-12-01 01:01:30 UTC'});
|
||||
|
||||
assert.deepEqual(result, 'published');
|
||||
});
|
||||
|
||||
it('can get date for draft post', function () {
|
||||
const result = JSONToHTML.getPostStatus({});
|
||||
|
||||
assert.deepEqual(result, 'draft');
|
||||
});
|
||||
});
|
||||
|
||||
describe('itemsToHtml', function () {
|
||||
it('can handle header item', function () {
|
||||
const result = JSONToHTML.itemsToHtml([{title: 'Hello World',issue_id: 123456,item_type: 'header',url: '',description: '',order: 0}]);
|
||||
|
||||
assert.deepEqual(result, '<h3>Hello World</h3>');
|
||||
});
|
||||
|
||||
it('can handle link item', function () {
|
||||
const result = JSONToHTML.itemsToHtml([{title: 'Google',issue_id: 123456,item_type: 'link',url: 'https://google.com/',description: 'A search engine.',order: 0,image: 'https://s3.amazonaws.com/revue/items/images/012/345/678/web/google.png?1234556'}]);
|
||||
|
||||
assert.deepEqual(result, '<figure class="kg-card kg-image-card kg-card-hascaption"><a href="https://google.com/"><img src="https://s3.amazonaws.com/revue/items/images/012/345/678/web/google.png?1234556" class="kg-image" alt loading="lazy"></a><figcaption>Google</figcaption></figure>\n' +
|
||||
'<h4><a href="https://google.com/">Google</a></h4>A search engine.');
|
||||
});
|
||||
|
||||
it('can handle link item with html in description', function () {
|
||||
const result = JSONToHTML.itemsToHtml([{title: 'Google',issue_id: 123456,item_type: 'link',url: 'https://google.com/',description: '<p>A <b>search</b> engine.</p>',order: 0,image: 'https://s3.amazonaws.com/revue/items/images/012/345/678/web/google.png?1234556'}]);
|
||||
|
||||
assert.deepEqual(result, '<figure class="kg-card kg-image-card kg-card-hascaption"><a href="https://google.com/"><img src="https://s3.amazonaws.com/revue/items/images/012/345/678/web/google.png?1234556" class="kg-image" alt loading="lazy"></a><figcaption>Google</figcaption></figure>\n' +
|
||||
'<h4><a href="https://google.com/">Google</a></h4><p>A <b>search</b> engine.</p>');
|
||||
});
|
||||
|
||||
it('can handle image item', function () {
|
||||
const result = JSONToHTML.itemsToHtml([{title: '', issue_id: 123456, item_type: 'image', url: '', description: 'Hello', order: 0, image: 'https://s3.amazonaws.com/revue/items/images/012/345/678/web/google.png?1234556', original_image_url: 'https://s3.amazonaws.com/revue/items/images/012/345/678/original/google.png?1234556'}]);
|
||||
|
||||
assert.deepEqual(result, '<figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://s3.amazonaws.com/revue/items/images/012/345/678/web/google.png?1234556" class="kg-image" alt loading="lazy"><figcaption>Hello</figcaption></figure>');
|
||||
});
|
||||
|
||||
it('can handle tweet item', function () {
|
||||
const result = JSONToHTML.itemsToHtml([{title: 'Ghost',issue_id: 123456,item_type: 'tweet',url: 'https://twitter.com/Ghost/status/123456',description: 'Hello world',order: 0,tweet_profile_image: 'https://s3.amazonaws.com/revue/tweet_items/profile_images/000/123/456/thumb/ABCD_normal.png?12345',tweet_handle: 'Ghost',tweet_description: '\u003cspan\u003eHello world\u003c/span\u003e',tweet_lang: 'en'}]);
|
||||
|
||||
assert.deepEqual(result, '<figure class="kg-card kg-embed-card">\n' +
|
||||
' <blockquote class="twitter-tweet"><a href="https://twitter.com/Ghost/status/123456"></a></blockquote>\n' +
|
||||
' <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>\n' +
|
||||
' </figure>');
|
||||
});
|
||||
|
||||
it('can handle long youtube video item', function () {
|
||||
const result = JSONToHTML.itemsToHtml([{title: '', issue_id: 123456, item_type: 'video', url: 'https://www.youtube.com/watch?v=ABCDEF', description: 'Hello World', order: 0, image: 'https://s3.amazonaws.com/revue/items/images/012/345/678/web/maxresdefault.jpg?1667924432'}]);
|
||||
|
||||
assert.deepEqual(result, '<figure class="kg-card kg-embed-card kg-card-hascaption"><iframe width="200" height="113" src="https://www.youtube.com/embed/ABCDEF?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><figcaption>Hello World</figcaption></figure>');
|
||||
});
|
||||
|
||||
it('can handle short youtube video item', function () {
|
||||
const result = JSONToHTML.itemsToHtml([{title: '', issue_id: 123456, item_type: 'video', url: 'https://youtu.be/ABCDEF', description: 'Hello World', order: 2, image: 'https://s3.amazonaws.com/revue/items/images/006/606/464/web/maxresdefault.jpg?1601883862'}]);
|
||||
|
||||
assert.deepEqual(result, '<figure class="kg-card kg-embed-card kg-card-hascaption"><iframe width="200" height="113" src="https://www.youtube.com/embed/ABCDEF?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><figcaption>Hello World</figcaption></figure>');
|
||||
});
|
||||
|
||||
it('can handle vimeo video item', function () {
|
||||
const result = JSONToHTML.itemsToHtml([{title: '', issue_id: 123456, item_type: 'video', url: 'https://vimeo.com/789123', description: 'Hello world', order: 2, image: 'https://s3.amazonaws.com/revue/items/images/006/606/464/web/maxresdefault.jpg?1601883862'}]);
|
||||
|
||||
assert.deepEqual(result, '<figure class="kg-card kg-embed-card kg-card-hascaption"><iframe src="https://player.vimeo.com/video/789123" width="200" height="113" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe><figcaption>Hello world</figcaption></figure>');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
79
yarn.lock
79
yarn.lock
@ -4463,7 +4463,7 @@
|
||||
"@tryghost/root-utils" "^0.3.18"
|
||||
debug "^4.3.1"
|
||||
|
||||
"@tryghost/elasticsearch@^3.0.7":
|
||||
"@tryghost/elasticsearch@^3.0.2", "@tryghost/elasticsearch@^3.0.7":
|
||||
version "3.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/elasticsearch/-/elasticsearch-3.0.7.tgz#eae9f00e89a0066673ca2470c5215de0ae976bea"
|
||||
integrity sha512-Q9EW6mMqteFuItG3fyJHV7esqi1/3pBDGmsKGe08TaqfryHw4E/zptjn1zDMPK7EMt5/i7TDVFBwY3Co1dN/CA==
|
||||
@ -4536,7 +4536,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/http-cache-utils/-/http-cache-utils-0.1.5.tgz#bfb9d17c59e9f838073831c530eef9d65454716e"
|
||||
integrity sha512-q829lqTxQkaPuup9ud7YjTQRQm1i/+bGhxElq0g10GMhEZqzqtDExb8fa/bAOL7CNwvUU7gdFMiT59/cyF9FmA==
|
||||
|
||||
"@tryghost/http-stream@^0.1.15":
|
||||
"@tryghost/http-stream@^0.1.10", "@tryghost/http-stream@^0.1.15":
|
||||
version "0.1.15"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/http-stream/-/http-stream-0.1.15.tgz#48cf656feb7917a6f62a18555abc0e2411eb8c8e"
|
||||
integrity sha512-O/SZP1HDstFqTYkpqfwEGWsDDvfLYOsNpGNz1Go7xtxo2qz94utzRFueqRDR9X/7pm2yP/1nQraHiVqTBb2YvA==
|
||||
@ -4580,7 +4580,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/kg-default-atoms/-/kg-default-atoms-3.1.4.tgz#58916cbd350e865246f95143d14ba62a06f7a1a7"
|
||||
integrity sha512-LBDW1uD70Wh27LiYzpctvIv6MExcgq7KkGy/RWUo0K/1EExtyEjLhNeeBrJ9taPouW2AQVwkqu69RHQ7NJW6eA==
|
||||
|
||||
"@tryghost/kg-default-cards@5.18.7":
|
||||
"@tryghost/kg-default-cards@5.18.7", "@tryghost/kg-default-cards@^5.18.7":
|
||||
version "5.18.7"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/kg-default-cards/-/kg-default-cards-5.18.7.tgz#2e1882cc543818cdbe9913f0b40891541369cdfa"
|
||||
integrity sha512-Fezi9DnAkuxuBEYErJi2FC3xXoqsdKHTutEvHMVMAFO38Ns3FQtDaoxBhDxBsmQawboS+ZfpaaTOJquEDtmoQg==
|
||||
@ -4660,7 +4660,24 @@
|
||||
lodash "^4.17.21"
|
||||
luxon "^1.26.0"
|
||||
|
||||
"@tryghost/logging@2.2.3", "@tryghost/logging@2.3.5", "@tryghost/logging@^2.2.3":
|
||||
"@tryghost/logging@2.2.3":
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/logging/-/logging-2.2.3.tgz#40575a42e18b907a49cee5dfdfa62deb820954aa"
|
||||
integrity sha512-ACCm84U4HITt3mQhDSpyDLZetxzjYo4ux2MoSVGL3zxPfQBPFoI6hWEiSzYWX/4RGq2l2tR4di+5LWjIe8Ow6A==
|
||||
dependencies:
|
||||
"@tryghost/bunyan-rotating-filestream" "^0.0.7"
|
||||
"@tryghost/elasticsearch" "^3.0.2"
|
||||
"@tryghost/http-stream" "^0.1.10"
|
||||
"@tryghost/pretty-stream" "^0.1.11"
|
||||
"@tryghost/root-utils" "^0.3.15"
|
||||
bunyan "^1.8.15"
|
||||
bunyan-loggly "^1.4.2"
|
||||
fs-extra "^10.0.0"
|
||||
gelf-stream "^1.1.1"
|
||||
json-stringify-safe "^5.0.1"
|
||||
lodash "^4.17.21"
|
||||
|
||||
"@tryghost/logging@2.3.5", "@tryghost/logging@^2.2.3":
|
||||
version "2.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/logging/-/logging-2.3.5.tgz#76806c21190d43008750dfb3e88cbe3558145511"
|
||||
integrity sha512-/rZ4CrBG1mi/WZXT86cXUadGOkTbdio4mo1csDft8SUfA/NYC7jm8TlfApeUn/O3CWiEO4vuTFeVlM8i4k4log==
|
||||
@ -4765,7 +4782,7 @@
|
||||
chalk "^4.1.0"
|
||||
sywac "^1.3.0"
|
||||
|
||||
"@tryghost/pretty-stream@^0.1.14":
|
||||
"@tryghost/pretty-stream@^0.1.11", "@tryghost/pretty-stream@^0.1.14":
|
||||
version "0.1.14"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/pretty-stream/-/pretty-stream-0.1.14.tgz#c7e88bf3324c89335a3c52e869c228a76a294eec"
|
||||
integrity sha512-pzLKwCVZA+Mri1MqJWpWqQ+U0g+g7bTvDqCC5krqpHcgGcUz2Oy5cNLGsSH1XMeWS9afjbLKUBOqXMf6bbi16Q==
|
||||
@ -19549,18 +19566,52 @@ mock-knex@TryGhost/mock-knex#8ecb8c227bf463c991c3d820d33f59efc3ab9682:
|
||||
lodash "^4.14.2"
|
||||
semver "^5.3.0"
|
||||
|
||||
moment-timezone@0.5.23, moment-timezone@0.5.34, moment-timezone@^0.5.23, moment-timezone@^0.5.31, moment-timezone@^0.5.33:
|
||||
moment-timezone@0.5.23, moment-timezone@^0.5.23:
|
||||
version "0.5.23"
|
||||
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.23.tgz#7cbb00db2c14c71b19303cb47b0fb0a6d8651463"
|
||||
integrity sha512-WHFH85DkCfiNMDX5D3X7hpNH3/PUhjTGcD0U1SgfBGZxJ3qUmJh5FdvaFjcClxOvB3rzdfj4oRffbI38jEnC1w==
|
||||
dependencies:
|
||||
moment ">= 2.9.0"
|
||||
|
||||
moment@2.24.0, moment@2.27.0, moment@2.29.1, moment@2.29.3, moment@2.29.4, "moment@>= 2.9.0", moment@^2.10.2, moment@^2.18.1, moment@^2.19.3, moment@^2.27.0, moment@^2.29.1:
|
||||
moment-timezone@0.5.34:
|
||||
version "0.5.34"
|
||||
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c"
|
||||
integrity sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==
|
||||
dependencies:
|
||||
moment ">= 2.9.0"
|
||||
|
||||
moment-timezone@^0.5.31, moment-timezone@^0.5.33:
|
||||
version "0.5.40"
|
||||
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.40.tgz#c148f5149fd91dd3e29bf481abc8830ecba16b89"
|
||||
integrity sha512-tWfmNkRYmBkPJz5mr9GVDn9vRlVZOTe6yqY92rFxiOdWXbjaR0+9LwQnZGGuNR63X456NqmEkbskte8tWL5ePg==
|
||||
dependencies:
|
||||
moment ">= 2.9.0"
|
||||
|
||||
moment@2.24.0, "moment@>= 2.9.0", moment@^2.10.2, moment@^2.18.1, moment@^2.19.3:
|
||||
version "2.24.0"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
|
||||
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
|
||||
|
||||
moment@2.27.0:
|
||||
version "2.27.0"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d"
|
||||
integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==
|
||||
|
||||
moment@2.29.1:
|
||||
version "2.29.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
||||
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
|
||||
|
||||
moment@2.29.3:
|
||||
version "2.29.3"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.3.tgz#edd47411c322413999f7a5940d526de183c031f3"
|
||||
integrity sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==
|
||||
|
||||
moment@2.29.4, moment@^2.27.0, moment@^2.29.1:
|
||||
version "2.29.4"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
|
||||
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
|
||||
|
||||
moo@^0.5.0, moo@^0.5.1:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c"
|
||||
@ -20648,7 +20699,7 @@ pako@~1.0.5:
|
||||
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
||||
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
|
||||
|
||||
papaparse@5.3.2:
|
||||
papaparse@5.3.2, papaparse@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.3.2.tgz#d1abed498a0ee299f103130a6109720404fbd467"
|
||||
integrity sha512-6dNZu0Ki+gyV0eBsFKJhYr+MdQYAzFUGlBMNj3GNrmHxmz1lfRa24CjFObPXtjcetlOv5Ad299MhIK0znp3afw==
|
||||
@ -23997,6 +24048,18 @@ sinon@14.0.2:
|
||||
nise "^5.1.2"
|
||||
supports-color "^7.2.0"
|
||||
|
||||
sinon@15.0.0:
|
||||
version "15.0.0"
|
||||
resolved "https://registry.yarnpkg.com/sinon/-/sinon-15.0.0.tgz#651a641b45c0a57aabc8275343c7108cffc9c062"
|
||||
integrity sha512-pV97G1GbslaSJoSdy2F2z8uh5F+uPGp3ddOzA4JsBOUBLEQRz2OAqlKGRFTSh2KiqUCmHkzyAeu7R4x1Hx0wwg==
|
||||
dependencies:
|
||||
"@sinonjs/commons" "^2.0.0"
|
||||
"@sinonjs/fake-timers" "^9.1.2"
|
||||
"@sinonjs/samsam" "^7.0.1"
|
||||
diff "^5.0.0"
|
||||
nise "^5.1.2"
|
||||
supports-color "^7.2.0"
|
||||
|
||||
sinon@^9.0.0:
|
||||
version "9.2.4"
|
||||
resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.2.4.tgz#e55af4d3b174a4443a8762fa8421c2976683752b"
|
||||
|
Loading…
Reference in New Issue
Block a user