'use strict'; /* Dependencies. */ var extend = require('extend'); var bail = require('bail'); var vfile = require('vfile'); var trough = require('trough'); var string = require('x-is-string'); var func = require('x-is-function'); var plain = require('is-plain-obj'); /* Expose a frozen processor. */ module.exports = unified().freeze(); var slice = [].slice; var own = {}.hasOwnProperty; /* Process pipeline. */ var pipeline = trough().use(pipelineParse).use(pipelineRun).use(pipelineStringify); function pipelineParse(p, ctx) { ctx.tree = p.parse(ctx.file); } function pipelineRun(p, ctx, next) { p.run(ctx.tree, ctx.file, done); function done(err, tree, file) { if (err) { next(err); } else { ctx.tree = tree; ctx.file = file; next(); } } } function pipelineStringify(p, ctx) { ctx.file.contents = p.stringify(ctx.tree, ctx.file); } /* Function to create the first processor. */ function unified() { var attachers = []; var transformers = trough(); var namespace = {}; var frozen = false; var freezeIndex = -1; /* Data management. */ processor.data = data; /* Lock. */ processor.freeze = freeze; /* Plug-ins. */ processor.attachers = attachers; processor.use = use; /* API. */ processor.parse = parse; processor.stringify = stringify; processor.run = run; processor.runSync = runSync; processor.process = process; processor.processSync = processSync; /* Expose. */ return processor; /* Create a new processor based on the processor * in the current scope. */ function processor() { var destination = unified(); var length = attachers.length; var index = -1; while (++index < length) { destination.use.apply(null, attachers[index]); } destination.data(extend(true, {}, namespace)); return destination; } /* Freeze: used to signal a processor that has finished * configuration. * * For example, take unified itself. It’s frozen. * Plug-ins should not be added to it. Rather, it should * be extended, by invoking it, before modifying it. * * In essence, always invoke this when exporting a * processor. */ function freeze() { var values; var plugin; var options; var transformer; if (frozen) { return processor; } while (++freezeIndex < attachers.length) { values = attachers[freezeIndex]; plugin = values[0]; options = values[1]; transformer = null; if (options === false) { continue; } if (options === true) { values[1] = undefined; } transformer = plugin.apply(processor, values.slice(1)); if (func(transformer)) { transformers.use(transformer); } } frozen = true; freezeIndex = Infinity; return processor; } /* Data management. * Getter / setter for processor-specific informtion. */ function data(key, value) { if (string(key)) { /* Set `key`. */ if (arguments.length === 2) { assertUnfrozen('data', frozen); namespace[key] = value; return processor; } /* Get `key`. */ return (own.call(namespace, key) && namespace[key]) || null; } /* Set space. */ if (key) { assertUnfrozen('data', frozen); namespace = key; return processor; } /* Get space. */ return namespace; } /* Plug-in management. * * Pass it: * * an attacher and options, * * a preset, * * a list of presets, attachers, and arguments (list * of attachers and options). */ function use(value) { var settings; assertUnfrozen('use', frozen); if (value === null || value === undefined) { /* Empty */ } else if (func(value)) { addPlugin.apply(null, arguments); } else if (typeof value === 'object') { if ('length' in value) { addList(value); } else { addPreset(value); } } else { throw new Error('Expected usable value, not `' + value + '`'); } if (settings) { namespace.settings = extend(namespace.settings || {}, settings); } return processor; function addPreset(result) { addList(result.plugins); if (result.settings) { settings = extend(settings || {}, result.settings); } } function add(value) { if (func(value)) { addPlugin(value); } else if (typeof value === 'object') { if ('length' in value) { addPlugin.apply(null, value); } else { addPreset(value); } } else { throw new Error('Expected usable value, not `' + value + '`'); } } function addList(plugins) { var length; var index; if (plugins === null || plugins === undefined) { /* Empty */ } else if (typeof plugins === 'object' && 'length' in plugins) { length = plugins.length; index = -1; while (++index < length) { add(plugins[index]); } } else { throw new Error('Expected a list of plugins, not `' + plugins + '`'); } } function addPlugin(plugin, value) { var entry = find(plugin); if (entry) { if (plain(entry[1]) && plain(value)) { value = extend(entry[1], value); } entry[1] = value; } else { attachers.push(slice.call(arguments)); } } } function find(plugin) { var length = attachers.length; var index = -1; var entry; while (++index < length) { entry = attachers[index]; if (entry[0] === plugin) { return entry; } } } /* Parse a file (in string or VFile representation) * into a Unist node using the `Parser` on the * processor. */ function parse(doc) { var file = vfile(doc); var Parser; freeze(); Parser = processor.Parser; assertParser('parse', Parser); if (newable(Parser)) { return new Parser(String(file), file).parse(); } return Parser(String(file), file); // eslint-disable-line new-cap } /* Run transforms on a Unist node representation of a file * (in string or VFile representation), async. */ function run(node, file, cb) { assertNode(node); freeze(); if (!cb && func(file)) { cb = file; file = null; } if (!cb) { return new Promise(executor); } executor(null, cb); function executor(resolve, reject) { transformers.run(node, vfile(file), done); function done(err, tree, file) { tree = tree || node; if (err) { reject(err); } else if (resolve) { resolve(tree); } else { cb(null, tree, file); } } } } /* Run transforms on a Unist node representation of a file * (in string or VFile representation), sync. */ function runSync(node, file) { var complete = false; var result; run(node, file, done); assertDone('runSync', 'run', complete); return result; function done(err, tree) { complete = true; bail(err); result = tree; } } /* Stringify a Unist node representation of a file * (in string or VFile representation) into a string * using the `Compiler` on the processor. */ function stringify(node, doc) { var file = vfile(doc); var Compiler; freeze(); Compiler = processor.Compiler; assertCompiler('stringify', Compiler); assertNode(node); if (newable(Compiler)) { return new Compiler(node, file).compile(); } return Compiler(node, file); // eslint-disable-line new-cap } /* Parse a file (in string or VFile representation) * into a Unist node using the `Parser` on the processor, * then run transforms on that node, and compile the * resulting node using the `Compiler` on the processor, * and store that result on the VFile. */ function process(doc, cb) { freeze(); assertParser('process', processor.Parser); assertCompiler('process', processor.Compiler); if (!cb) { return new Promise(executor); } executor(null, cb); function executor(resolve, reject) { var file = vfile(doc); pipeline.run(processor, {file: file}, done); function done(err) { if (err) { reject(err); } else if (resolve) { resolve(file); } else { cb(null, file); } } } } /* Process the given document (in string or VFile * representation), sync. */ function processSync(doc) { var complete = false; var file; freeze(); assertParser('processSync', processor.Parser); assertCompiler('processSync', processor.Compiler); file = vfile(doc); process(file, done); assertDone('processSync', 'process', complete); return file; function done(err) { complete = true; bail(err); } } } /* Check if `func` is a constructor. */ function newable(value) { return func(value) && keys(value.prototype); } /* Check if `value` is an object with keys. */ function keys(value) { var key; for (key in value) { return true; } return false; } /* Assert a parser is available. */ function assertParser(name, Parser) { if (!func(Parser)) { throw new Error('Cannot `' + name + '` without `Parser`'); } } /* Assert a compiler is available. */ function assertCompiler(name, Compiler) { if (!func(Compiler)) { throw new Error('Cannot `' + name + '` without `Compiler`'); } } /* Assert the processor is not frozen. */ function assertUnfrozen(name, frozen) { if (frozen) { throw new Error( 'Cannot invoke `' + name + '` on a frozen processor.\n' + 'Create a new processor first, by invoking it: ' + 'use `processor()` instead of `processor`.' ); } } /* Assert `node` is a Unist node. */ function assertNode(node) { if (!node || !string(node.type)) { throw new Error('Expected node, got `' + node + '`'); } } /* Assert that `complete` is `true`. */ function assertDone(name, asyncName, complete) { if (!complete) { throw new Error('`' + name + '` finished async. Use `' + asyncName + '` instead'); } }