UNPKG

gatsby-rehype-inline-images

Version:

Gatsby plugin to download external images

335 lines (280 loc) 7.6 kB
"use strict"; const _ = require(`lodash`); const visit = require(`unist-util-visit-parents`); const { createRemoteFileNode } = require(`gatsby-source-filesystem`); const { fluid } = require(`gatsby-plugin-sharp`); const sharp = require(`sharp`); const fs = require(`fs-extra`); const path = require(`path`); const supportedExtensions = { jpeg: true, jpg: true, png: true, webp: true, tif: true, tiff: true }; const fileCacheKey = url => `gatsby-rehype-inline-images-file-${url}`; const fluidImageCacheKey = key => `gatsby-rehype-inline-images-fluid-image-${key}`; const getContext = (node, field) => node && node.context && node.context[field]; const copyToStatic = async ({ file, pathPrefix = `` }) => { const fileName = `${file.internal.contentDigest}/${file.base}`; const publicPath = path.join(process.cwd(), `public`, `static`, fileName); let fileExists; try { fileExists = await fs.stat(publicPath); } catch { fileExists = false; } if (!fileExists) { try { await fs.copy(file.absolutePath, publicPath, { dereference: true }); } catch (err) { console.error(`error copying file from ${file.absolutePath} to ${publicPath}`, err); } } return `${pathPrefix}/static/${fileName}`; }; exports.pluginOptionsSchema = ({ Joi }) => { return Joi.object({ withWebp: Joi.boolean().default(true).description(`Enables webP.`), useImageCache: Joi.boolean().default(true).description(`Enables Image Cache.`) }).external(async (pluginParams, pluginOptions) => { const { htmlAst, htmlNode, reporter } = pluginParams; const url = getContext(htmlNode, `url`); const slug = getContext(htmlNode, `slug`); if (!url && slug) { reporter.warn(`Expected url and slug not defined.`); return htmlAst; } function isUrl(s) { const regexp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/; return regexp.test(s); } const cmsUrl = _.head(_.split(url, slug, 1)); if (!isUrl(cmsUrl)) { return htmlAst; } const nodes = []; visit(htmlAst, { tagName: `img` }, async (node, ancestor) => { const directAncestor = ancestor.reverse()[0]; nodes.push({ node, ancestor: directAncestor }); reporter.info(node.properties.src); }); const images = await Promise.all(nodes.map(({ node }) => replaceNewImage(node, pluginParams, pluginOptions))); images.forEach((image, i) => { if (image) { const { node, ancestor } = nodes[i]; // save original tag to special property node.properties.htmlTag = node.tagName; // add new tag and properties for React component node.tagName = `img-sharp-inline`; node.properties.className = image.className; node.properties.fluidImg = JSON.stringify(image.fluid); node.properties.alt = image.alt; node.properties.maxWidth = image.maxWidth; // add new class to parent for styling const fluidClass = `fluid-image`; const parentClass = ancestor.properties.className; ancestor.properties.className = Array.isArray(parentClass) && [...parentClass, fluidClass] || [fluidClass]; node.properties.parentClassName = ancestor.properties.className; // add flex style to parent to keep aspect ratio const flex = `flex: ${image.aspectRatio} 1 0`; const parentStyle = ancestor.properties.style; ancestor.properties.style = Array.isArray(parentStyle) && [...parentStyle, flex] || [flex]; // do not include these props in html output node.properties.htmlClearProps = [`className`, `fluidImg`, `parentClassName`]; } }); return htmlAst; }); }; const replaceNewImage = async (node, pluginParams, pluginOptions) => { const url = node.properties.src.replace(/^\/\//, `https://`); if (!url) { return false; } let fileNode; try { fileNode = await downloadMediaFile({ url, pluginParams }); } catch (e) { throw Error(e); } if (!fileNode) { return false; } // unsupported image files are copied to the `/static` folder if (!supportedExtensions[fileNode.extension]) { const { pathPrefix } = pluginParams; const src = await copyToStatic({ file: fileNode, pathPrefix }); // mutate here node.properties.src = src; return false; } // process supported images return await processImage({ fileNode, node, pluginParams, pluginOptions }); }; const processImage = async ({ fileNode, node, pluginParams, pluginOptions }) => { const image = { url: node.properties.src }; const classList = node.properties.className || []; image.alt = node.properties.alt || fileNode.name; image.className = classList.join(` `); const { fluidResult, aspectRatio, maxWidth } = await fluidImage({ fileNode, pluginParams, pluginOptions }); image.fluid = fluidResult; image.aspectRatio = aspectRatio; image.maxWidth = maxWidth; return image; }; // imageCache can be disabled if you need to save memory const fluidImage = async ({ fileNode, pluginParams, pluginOptions }) => { const { cache, reporter } = pluginParams; const { withWebp = true, useImageCache = true } = pluginOptions; if (!fileNode || !fileNode.absolutePath) { return false; } const selector = `${fileNode.id}${withWebp ? `-webp` : ``}`; const fluidImageKey = fluidImageCacheKey(selector); const cachedFluidImage = await cache.get(fluidImageKey); if (useImageCache && cachedFluidImage && cachedFluidImage.length > 0) { return JSON.parse(cachedFluidImage); } try { const { width, height } = await sharp(fileNode.absolutePath).metadata(); const maxWidth = pluginOptions.maxWidth ? Math.min(pluginOptions.maxWidth, width) : width; const aspectRatio = `${width / height}`; const fluidParams = { file: fileNode, args: { ...pluginOptions, maxWidth }, reporter, cache }; let fluidResult = await fluid(fluidParams); let fluidResultWebp; if (withWebp) { fluidParams.args.toFormat = `webp`; fluidResultWebp = await fluid(fluidParams); } if (!fluidResult) { return false; } if (withWebp) { fluidResult.srcSetWebp = fluidResultWebp.srcSet; } if (useImageCache) { await cache.set(fluidImageKey, JSON.stringify(fluidResult)); } return { fluidResult, aspectRatio, maxWidth }; } catch (e) { throw Error(e); } }; const downloadMediaFile = async ({ url, pluginParams }) => { const { cache, store, actions: { createNode, touchNode }, createNodeId, getNode } = pluginParams; const cacheKey = fileCacheKey(url); const cachedFileData = await cache.get(cacheKey); let fileNode = false; if (cachedFileData && cachedFileData.fileNodeId) { const fileNodeId = cachedFileData.fileNodeId; fileNode = getNode(fileNodeId); if (fileNode) { touchNode({ nodeId: fileNodeId }); } } if (fileNode) { return fileNode; } try { fileNode = await createRemoteFileNode({ url, store, cache, createNode, createNodeId }); if (fileNode) { await cache.set(cacheKey, { fileNodeId: fileNode.id }); } } catch (e) { throw Error(e); } return fileNode; };