const path = require('path'); const errors = require('@tryghost/errors'); const logging = require('../../shared/logging'); const config = require('../../shared/config'); const storage = require('../adapters/storage'); const imageTransform = require('@tryghost/image-transform'); let cardFactory; let cards; let mobiledocHtmlRenderer; module.exports = { get blankDocument() { return { version: '0.3.1', markups: [], atoms: [], cards: [], sections: [ [1, 'p', [ [0, [], 0, ''] ]] ] }; }, get cards() { if (!cards) { const CardFactory = require('@tryghost/kg-card-factory'); const defaultCards = require('@tryghost/kg-default-cards'); cardFactory = new CardFactory({ siteUrl: config.get('url'), contentImageSizes: config.get('imageOptimization:contentImageSizes'), srcsets: config.get('imageOptimization:srcsets'), canTransformImage(storagePath) { const {ext} = path.parse(storagePath); return imageTransform.canTransformFiles() && imageTransform.canTransformFileExtension(ext) && typeof storage.getStorage().saveRaw === 'function'; } }); cards = defaultCards.map((card) => { return cardFactory.createCard(card); }); } return cards; }, get atoms() { return require('@tryghost/kg-default-atoms'); }, get mobiledocHtmlRenderer() { if (!mobiledocHtmlRenderer) { const MobiledocHtmlRenderer = require('@tryghost/kg-mobiledoc-html-renderer'); mobiledocHtmlRenderer = new MobiledocHtmlRenderer({ cards: this.cards, atoms: this.atoms, unknownCardHandler(args) { logging.error(new errors.InternalServerError({ message: 'Mobiledoc card \'' + args.env.name + '\' not found.' })); } }); } return mobiledocHtmlRenderer; }, get htmlToMobiledocConverter() { try { return require('@tryghost/html-to-mobiledoc').toMobiledoc; } catch (err) { return () => { throw new errors.InternalServerError({ message: 'Unable to convert from source HTML to Mobiledoc', context: 'The html-to-mobiledoc package was not installed', help: 'Please review any errors from the install process by checking the Ghost logs', code: 'HTML_TO_MOBILEDOC_INSTALLATION', err: err }); }; } }, // used when force-rerendering post content to ensure that old image card // payloads contain width/height values to be used when generating srcsets populateImageSizes: async function (mobiledocJson) { // do not require image-size until it's requested to avoid circular dependencies // shared/url-utils > server/lib/mobiledoc > server/lib/image/image-size > server/adapters/storage/utils const imageSize = require('./image/image-size'); const urlUtils = require('../../shared/url-utils'); const storageInstance = storage.getStorage(); async function getUnsplashSize(url) { const parsedUrl = new URL(url); parsedUrl.searchParams.delete('w'); parsedUrl.searchParams.delete('fit'); parsedUrl.searchParams.delete('crop'); parsedUrl.searchParams.delete('dpr'); return await imageSize.getImageSizeFromUrl(parsedUrl.href); } async function getLocalSize(url) { // local storage adapter's .exists() expects image paths without any prefixes const subdirRegex = new RegExp(`^${urlUtils.getSubdir()}`); const contentRegex = new RegExp(`^/${urlUtils.STATIC_IMAGE_URL_PREFIX}`); const storagePath = url.replace(subdirRegex, '').replace(contentRegex, ''); const {dir, name, ext} = path.parse(storagePath); const [imageNameMatched, imageName, imageNumber] = name.match(/^(.+?)(-\d+)?$/) || [null]; if (!imageNameMatched || !(await storageInstance.exists(storagePath))) { return; } // get the original/unoptimized image if it exists as that will have // the maximum dimensions that srcset/handle-image-sizes can use const originalImagePath = path.join(dir, `${imageName}_o${imageNumber || ''}${ext}`); const imagePath = await storageInstance.exists(originalImagePath) ? originalImagePath : storagePath; return await imageSize.getImageSizeFromStoragePath(imagePath); } const mobiledoc = JSON.parse(mobiledocJson); const sizePromises = mobiledoc.cards.map(async (card) => { const [cardName, payload] = card; const needsFilling = cardName === 'image' && payload && payload.src && (!payload.width || !payload.height); if (!needsFilling) { return; } const isUnsplash = payload.src.match(/images\.unsplash\.com/); try { const size = isUnsplash ? await getUnsplashSize(payload.src) : await getLocalSize(payload.src); if (size && size.width && size.height) { payload.width = size.width; payload.height = size.height; } } catch (e) { // TODO: use debug instead? logging.error(e); } }); await Promise.all(sizePromises); return JSON.stringify(mobiledoc); }, // allow config changes to be picked up - useful in tests reload() { cardFactory = null; cards = null; mobiledocHtmlRenderer = null; } };