UNPKG

remark-copy-linked-files

Version:

Find files which are linked to from markdown and copy them to the public directory

209 lines (179 loc) 5.93 kB
const cheerio = require('cheerio'); const Cp = require('cp-file'); const { default: ForEach } = require('apr-for-each'); const Reduce = require('apr-reduce'); const Intercept = require('apr-intercept'); const isRelativeUrl = require('is-relative-url'); const { readFile, exists } = require('mz/fs'); const { dirname, resolve, basename, extname, join, sep } = require('path'); const revHash = require('rev-hash'); const UniqBy = require('lodash.uniqby'); // https://github.com/syntax-tree/unist-util-map/blob/bb0567f651517b2d521af711d7376475b3d8446a/index.js const map = async (tree, iteratee) => { const bound = (node) => async (child, index) => { return preorder(child, index, node); }; const preorder = async (node, index, parent) => { const [, newNode = {}] = await Intercept(iteratee(node, index, parent)); const { children = [] } = newNode || node; return { ...node, ...newNode, children: await Promise.all(children.map(bound(node))), }; }; return preorder(tree, null, null); }; const defaultUrlBuilder = ({ filename, staticPath }) => { return resolve('/', staticPath, filename); }; module.exports = (opts = {}) => { const { destinationDir, staticPath = '/', ignoreFileExtensions = [], buildUrl = defaultUrlBuilder, selectors: customSelectors = [], transformAsset, } = opts; return async (tree, { cwd, path: cPath, pathname: cPathname }) => { const path = cPath || cPathname; // #121 const assets = []; const handleUrl = async (url) => { const platformNormalizedUrl = url.replace(/[\\/]/g, sep); if (!isRelativeUrl(platformNormalizedUrl)) { return; } const ext = extname(platformNormalizedUrl); if (!ext || ignoreFileExtensions.includes(ext)) { return; } const fullpath = resolve( cwd, path ? dirname(path) : '', platformNormalizedUrl, ); if (!(await exists(fullpath))) { return; } const rev = revHash(await readFile(fullpath)); const name = basename(fullpath, ext); const filename = `${name}-${rev}${ext}`; return { fullpath, filename, url: buildUrl({ staticPath, filename, fullpath, name, rev, }), }; }; const handlers = { html: async (node, { selectors: sels = [] } = {}) => { let { value: newValue = '' } = node; const $ = cheerio.load(newValue); const selectors = [ ['img[src]', 'src'], ['video source[src]', 'src'], ['video[src]', 'src'], ['audio source[src]', 'src'], ['audio[src]', 'src'], ['video[poster]', 'poster'], ['object param[value]', 'value'], ['a[href]', 'href'], ].concat(sels); const urls = selectors .concat(customSelectors) .reduce((memo, [selector, attr]) => { return memo.concat( $(selector) .toArray() .map(({ attribs }) => attribs[attr]), ); }, []); await ForEach(urls, async (url) => { const nUrl = await handleUrl(url); const asset = transformAsset ? await transformAsset(nUrl) : nUrl; if (!asset) { return; } assets.push(asset); const { url: newUrl } = asset; newValue = newValue.replace(new RegExp(url, `g`), newUrl); }); return Object.assign(node, { value: newValue, }); }, url: async (node) => { const nUrl = await handleUrl(node.url); const asset = transformAsset ? await transformAsset(nUrl) : nUrl; assets.push(asset); return Object.assign(node, { url: asset ? asset.url || node.url : node.url, }); }, link: (...args) => handlers.url(...args), definition: (...args) => handlers.url(...args), image: (...args) => handlers.url(...args), jsx: (...args) => { return handlers.html(...args, { selectors: [ ['[poster]', 'poster'], ['[src]', 'src'], ['[href]', 'href'], ], }); }, mdxBlockElement: async (node) => { const { attributes = [] } = node; const selectors = ['poster', 'src', 'href', 'value']; return Reduce( attributes, async (node, attr, atIndex) => { const { name, value } = attr; const { attributes = [] } = node; const selector = selectors.find((selector) => { return selector === name; }); if (!selector) { return node; } const nUrl = await handleUrl(value); const asset = transformAsset ? await transformAsset(nUrl) : nUrl; assets.push(asset); return Object.assign(node, { attributes: attributes.map((item, index) => { return index === atIndex ? { ...attr, value: asset ? asset.url || value : value } : item; }), }); }, node, ); }, mdxSpanElement: (...args) => handlers.mdxBlockElement(...args), mdxJsxFlowElement: (...args) => handlers.mdxBlockElement(...args), mdxJsxTextElement: (...args) => handlers.mdxBlockElement(...args), }; const newTree = await map(tree, async (node) => { try { return handlers[node.type] ? handlers[node.type](node) : node; } catch (err) { console.error(err); return node; } }); await ForEach( UniqBy(assets.filter(Boolean), 'filename'), async ({ fullpath, filename }) => { await Cp(fullpath, join(destinationDir, filename)); }, ); return newTree; }; };