@metalsmith/markdown
Version:
A Metalsmith plugin to render markdown files to HTML
197 lines (188 loc) • 7.22 kB
JavaScript
import { dirname, basename, extname, join } from 'path';
import get from 'dlv';
import { dset } from 'dset';
import { marked } from 'marked';
// istanbul ignore next
function error(name, msg) {
const err = new Error(msg);
err.name = name;
return err;
}
function isArray(arg) {
return Array.isArray(arg);
}
function isString(arg) {
return typeof arg === 'string';
}
function isObject(arg) {
return typeof arg === 'object' && arg !== null;
}
/**
* Expand wildcard `char` for `keypaths` in `root`. The results can be used by a utility function like lodash.get or dlv. For example:
* ```js
* let keypaths = [
* ['arr.*'],
* ['arr.*.*']
* ]
* expand(keypaths, { arr: ['a','b','c']}) // => [['arr', 0], ['arr', 1], ['arr', 2]]
* expand(keypaths, { arr: ['a','b','c']}) // => [['arr', 0], ['arr', 1], ['arr', 2]]
* ```
* @param {Object|Array} root
* @param {Array<string|number>[]} keypaths
* @param {string} [char='*']
* @returns {Array<string|number>[]}
*/
function expandWildcardKeypath(root, keypaths, char) {
// istanbul ignore if
if (!isObject(root)) {
throw error('EINVALID_ARGUMENT', 'root must be an object or array');
}
// istanbul ignore if
if (!isArray(keypaths) || keypaths.filter(keypath => !isString(keypath) && !isArray(keypath)).length) {
throw error('EINVALID_ARGUMENT', 'keypaths must be strings or arrays of strings');
}
const expanded = keypaths.reduce((result, keypath) => {
if (isString(keypath)) keypath = keypath.split('.');
const wildcard = keypath.indexOf(char);
if (wildcard > -1) {
const pre = keypath.slice(0, wildcard);
const wildcardRoot = get(root, pre);
if (!wildcardRoot) return result;
const looped = isArray(wildcardRoot) ? wildcardRoot : Object.keys(wildcardRoot);
looped.forEach((entry, index) => {
const pp = Array.from(keypath);
pp.splice(wildcard, 1, isArray(wildcardRoot) ? index : entry);
result.push(pp);
});
} else {
result.push(keypath);
}
return result;
}, []);
if (expanded.find(entry => entry.indexOf(char) > -1)) {
return expandWildcardKeypath(root, expanded, char);
}
return expanded;
}
function defaultRender(source, options) {
return marked(source, options);
}
function refsObjectToMarkdown(refsObject) {
return Object.entries(refsObject).map(([refname, value]) => `[${refname}]: ${value}`).join('\n');
}
/**
* @callback Render
* @param {string} source
* @param {Object} engineOptions
* @param {{ path: string, key: string}} context
*/
/**
* @typedef Options
* @property {string[]|{files: string[], global: string[]}} [keys] - Key names of file metadata to render to HTML - can be nested
* @property {boolean} [wildcard=false] - Expand `*` wildcards in keypaths
* @property {string|Object<string, string>} [globalRefs] An object of `{ refname: 'link' }` pairs that will be made available for all markdown files and keys,
* or a `metalsmith.metadata()` keypath containing such object
* @property {Render} [render] - Specify a custom render function with the signature `(source, engineOptions, context) => string`.
* `context` is an object with a `path` key containing the current file path, and `key` containing the target key.
* @property {Object} [engineOptions] Options to pass to the markdown engine (default [marked](https://github.com/markedjs/marked))
**/
const defaultOptions = {
keys: {},
wildcard: false,
render: defaultRender,
engineOptions: {},
globalRefs: {}
};
/**
* A Metalsmith plugin to render markdown files to HTML
* @param {Options} [options]
* @return {import('metalsmith').Plugin}
*/
function markdown(options = defaultOptions) {
if (options === true) {
options = defaultOptions;
} else {
options = Object.assign({}, defaultOptions, options);
}
if (Array.isArray(options.keys)) {
options.keys = {
files: options.keys
};
}
return function markdown(files, metalsmith, done) {
const debug = metalsmith.debug('@metalsmith/markdown');
const matches = metalsmith.match('**/*.{md,markdown}', Object.keys(files));
function renderKeys(keys, prepend, target, path) {
if (options.wildcard) {
keys = expandWildcardKeypath(target, keys, '*');
}
keys.forEach(key => {
const value = get(target, key);
if (typeof value === 'string') {
const context = path === 'metalsmith.metadata()' ? {
key
} : {
path,
key
};
debug.info('Rendering key "%s" of target "%s"', key.join ? key.join('.') : key, path);
dset(target, key, options.render(prepend + value, options.engineOptions, context));
} else if (typeof value !== 'undefined') {
debug.warn('Couldn\'t render key "%s" of target "%s": not a string', key.join ? key.join('.') : key, path);
}
});
}
const legacyEngineOptions = Object.keys(options).filter(opt => !Object.keys(defaultOptions).includes(opt));
if (legacyEngineOptions.length) {
debug.warn('Starting from version 2.0 marked engine options will need to be specified as options.engineOptions');
legacyEngineOptions.forEach(opt => {
options.engineOptions[opt] = options[opt];
});
debug.warn('Moved engine options %s to options.engineOptions', legacyEngineOptions.join(', '));
}
debug('Running with options: %O', options);
if (matches.length === 0) {
debug.warn('No markdown files found.');
} else {
debug('Processing %s markdown file(s)', matches.length);
}
let globalRefsMarkdown = '';
if (typeof options.globalRefs === 'string') {
const found = get(metalsmith.metadata(), options.globalRefs);
if (found) {
globalRefsMarkdown = refsObjectToMarkdown(found);
} else {
const err = new Error(`globalRefs not found in metalsmith.metadata().${options.globalRefs}`);
err.name = 'Error @metalsmith/markdown';
done(err);
}
} else if (typeof options.globalRefs === 'object' && options.globalRefs !== null) {
globalRefsMarkdown = refsObjectToMarkdown(options.globalRefs);
}
if (globalRefsMarkdown.length) globalRefsMarkdown += '\n\n';
matches.forEach(function (file) {
const data = files[file];
const dir = dirname(file);
let html = basename(file, extname(file)) + '.html';
if ('.' != dir) html = join(dir, html);
debug.info('Rendering file "%s" as "%s"', file, html);
const str = options.render(globalRefsMarkdown + data.contents.toString(), options.engineOptions, {
path: file,
key: 'contents'
});
data.contents = Buffer.from(str);
const keys = options.keys && options.keys.files ? options.keys.files : [];
renderKeys(keys, globalRefsMarkdown, data, file);
delete files[file];
files[html] = data;
});
if (options.keys && options.keys.global) {
debug.info('Processing metalsmith.metadata()');
const meta = metalsmith.metadata();
renderKeys(options.keys.global, globalRefsMarkdown, meta, 'metalsmith.metadata()');
}
done();
};
}
export { markdown as default };
//# sourceMappingURL=index.js.map