You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

461 lines
10 KiB

'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. Its 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');
}
}