mirror of https://github.com/lukechilds/node.git
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.
638 lines
14 KiB
638 lines
14 KiB
8 years ago
|
/**
|
||
|
* @author Titus Wormer
|
||
|
* @copyright 2015 Titus Wormer
|
||
|
* @license MIT
|
||
|
* @module unified
|
||
|
* @fileoverview Pluggable text processing interface.
|
||
|
*/
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
/* Dependencies. */
|
||
|
var events = require('events');
|
||
|
var has = require('has');
|
||
|
var once = require('once');
|
||
|
var extend = require('extend');
|
||
|
var bail = require('bail');
|
||
|
var vfile = require('vfile');
|
||
|
var trough = require('trough');
|
||
|
|
||
|
/* Expose an abstract processor. */
|
||
|
module.exports = unified().abstract();
|
||
|
|
||
|
/* Methods. */
|
||
|
var slice = [].slice;
|
||
|
|
||
|
/* Process pipeline. */
|
||
|
var pipeline = trough()
|
||
|
.use(function (p, ctx) {
|
||
|
ctx.tree = p.parse(ctx.file, ctx.options);
|
||
|
})
|
||
|
.use(function (p, ctx, next) {
|
||
|
p.run(ctx.tree, ctx.file, function (err, tree, file) {
|
||
|
if (err) {
|
||
|
next(err);
|
||
|
} else {
|
||
|
ctx.tree = tree;
|
||
|
ctx.file = file;
|
||
|
next();
|
||
|
}
|
||
|
});
|
||
|
})
|
||
|
.use(function (p, ctx) {
|
||
|
ctx.file.contents = p.stringify(ctx.tree, ctx.file, ctx.options);
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* Function to create the first processor.
|
||
|
*
|
||
|
* @return {Function} - First processor.
|
||
|
*/
|
||
|
function unified() {
|
||
|
var attachers = [];
|
||
|
var transformers = trough();
|
||
|
var namespace = {};
|
||
|
var chunks = [];
|
||
|
var emitter = new events.EventEmitter();
|
||
|
var ended = false;
|
||
|
var concrete = true;
|
||
|
var settings;
|
||
|
var key;
|
||
|
|
||
|
/**
|
||
|
* Create a new processor based on the processor
|
||
|
* in the current scope.
|
||
|
*
|
||
|
* @return {Processor} - New concrete processor based
|
||
|
* on the descendant processor.
|
||
|
*/
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
/* Mix in methods. */
|
||
|
for (key in emitter) {
|
||
|
processor[key] = emitter[key];
|
||
|
}
|
||
|
|
||
|
/* Helpers. */
|
||
|
|
||
|
/**
|
||
|
* Assert a parser is available.
|
||
|
*
|
||
|
* @param {string} name - Name of callee.
|
||
|
*/
|
||
|
function assertParser(name) {
|
||
|
if (!isParser(processor.Parser)) {
|
||
|
throw new Error('Cannot `' + name + '` without `Parser`');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Assert a compiler is available.
|
||
|
*
|
||
|
* @param {string} name - Name of callee.
|
||
|
*/
|
||
|
function assertCompiler(name) {
|
||
|
if (!isCompiler(processor.Compiler)) {
|
||
|
throw new Error('Cannot `' + name + '` without `Compiler`');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Assert the processor is concrete.
|
||
|
*
|
||
|
* @param {string} name - Name of callee.
|
||
|
*/
|
||
|
function assertConcrete(name) {
|
||
|
if (!concrete) {
|
||
|
throw new Error(
|
||
|
'Cannot ' +
|
||
|
(name ? 'invoke `' + name + '` on' : 'pipe into') +
|
||
|
' abstract processor.\n' +
|
||
|
'To make the processor concrete, invoke it: ' +
|
||
|
'use `processor()` instead of `processor`.'
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Assert `node` is a Unist node.
|
||
|
*
|
||
|
* @param {*} node - Value to check.
|
||
|
*/
|
||
|
function assertNode(node) {
|
||
|
if (!isNode(node)) {
|
||
|
throw new Error('Expected node, got `' + node + '`');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Assert, if no `done` is given, that `complete` is
|
||
|
* `true`.
|
||
|
*
|
||
|
* @param {string} name - Name of callee.
|
||
|
* @param {boolean} complete - Whether an async process
|
||
|
* is complete.
|
||
|
* @param {Function?} done - Optional handler of async
|
||
|
* results.
|
||
|
*/
|
||
|
function assertDone(name, complete, done) {
|
||
|
if (!complete && !done) {
|
||
|
throw new Error(
|
||
|
'Expected `done` to be given to `' + name + '` ' +
|
||
|
'as async plug-ins are used'
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/* Throw as early as possible.
|
||
|
* As events are triggered synchroneously, the stack
|
||
|
* is preserved. */
|
||
|
processor.on('pipe', function () {
|
||
|
assertConcrete();
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* Abstract: used to signal an abstract processor which
|
||
|
* should made concrete before using.
|
||
|
*
|
||
|
* For example, take unified itself. It’s abstract.
|
||
|
* Plug-ins should not be added to it. Rather, it should
|
||
|
* be made concrete (by invoking it) before modifying it.
|
||
|
*
|
||
|
* In essence, always invoke this when exporting a
|
||
|
* processor.
|
||
|
*
|
||
|
* @return {Processor} - The operated on processor.
|
||
|
*/
|
||
|
function abstract() {
|
||
|
concrete = false;
|
||
|
|
||
|
return processor;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Data management.
|
||
|
*
|
||
|
* Getter / setter for processor-specific informtion.
|
||
|
*
|
||
|
* @param {string} key - Key to get or set.
|
||
|
* @param {*} value - Value to set.
|
||
|
* @return {*} - Either the operator on processor in
|
||
|
* setter mode; or the value stored as `key` in
|
||
|
* getter mode.
|
||
|
*/
|
||
|
function data(key, value) {
|
||
|
assertConcrete('data');
|
||
|
|
||
|
if (typeof key === 'string') {
|
||
|
/* Set `key`. */
|
||
|
if (arguments.length === 2) {
|
||
|
namespace[key] = value;
|
||
|
|
||
|
return processor;
|
||
|
}
|
||
|
|
||
|
/* Get `key`. */
|
||
|
return (has(namespace, key) && namespace[key]) || null;
|
||
|
}
|
||
|
|
||
|
/* Get space. */
|
||
|
if (!key) {
|
||
|
return namespace;
|
||
|
}
|
||
|
|
||
|
/* Set space. */
|
||
|
namespace = key;
|
||
|
|
||
|
return processor;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Plug-in management.
|
||
|
*
|
||
|
* Pass it:
|
||
|
* * an attacher and options,
|
||
|
* * a list of attachers and options for all of them;
|
||
|
* * a tuple of one attacher and options.
|
||
|
* * a matrix: list containing any of the above and
|
||
|
* matrices.
|
||
|
*
|
||
|
* @param {...*} value - See description.
|
||
|
* @return {Processor} - The operated on processor.
|
||
|
*/
|
||
|
function use(value) {
|
||
|
var args = slice.call(arguments, 0);
|
||
|
var params = args.slice(1);
|
||
|
var index;
|
||
|
var length;
|
||
|
var transformer;
|
||
|
|
||
|
assertConcrete('use');
|
||
|
|
||
|
/* Multiple attachers. */
|
||
|
if ('length' in value && !isFunction(value)) {
|
||
|
index = -1;
|
||
|
length = value.length;
|
||
|
|
||
|
if (!isFunction(value[0])) {
|
||
|
/* Matrix of things. */
|
||
|
while (++index < length) {
|
||
|
use(value[index]);
|
||
|
}
|
||
|
} else if (isFunction(value[1])) {
|
||
|
/* List of things. */
|
||
|
while (++index < length) {
|
||
|
use.apply(null, [value[index]].concat(params));
|
||
|
}
|
||
|
} else {
|
||
|
/* Arguments. */
|
||
|
use.apply(null, value);
|
||
|
}
|
||
|
|
||
|
return processor;
|
||
|
}
|
||
|
|
||
|
/* Store attacher. */
|
||
|
attachers.push(args);
|
||
|
|
||
|
/* Single attacher. */
|
||
|
transformer = value.apply(null, [processor].concat(params));
|
||
|
|
||
|
if (isFunction(transformer)) {
|
||
|
transformers.use(transformer);
|
||
|
}
|
||
|
|
||
|
return processor;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse a file (in string or VFile representation)
|
||
|
* into a Unist node using the `Parser` on the
|
||
|
* processor.
|
||
|
*
|
||
|
* @param {(string|VFile)?} [file] - File to process.
|
||
|
* @param {Object?} [options] - Configuration.
|
||
|
* @return {Node} - Unist node.
|
||
|
*/
|
||
|
function parse(file, options) {
|
||
|
assertConcrete('parse');
|
||
|
assertParser('parse');
|
||
|
|
||
|
return new processor.Parser(vfile(file), options, processor).parse();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Run transforms on a Unist node representation of a file
|
||
|
* (in string or VFile representation).
|
||
|
*
|
||
|
* @param {Node} node - Unist node.
|
||
|
* @param {(string|VFile)?} [file] - File representation.
|
||
|
* @param {Function?} [done] - Callback.
|
||
|
* @return {Node} - The given or resulting Unist node.
|
||
|
*/
|
||
|
function run(node, file, done) {
|
||
|
var complete = false;
|
||
|
var result;
|
||
|
|
||
|
assertConcrete('run');
|
||
|
assertNode(node);
|
||
|
|
||
|
result = node;
|
||
|
|
||
|
if (!done && file && !isFile(file)) {
|
||
|
done = file;
|
||
|
file = null;
|
||
|
}
|
||
|
|
||
|
transformers.run(node, vfile(file), function (err, tree, file) {
|
||
|
complete = true;
|
||
|
result = tree || node;
|
||
|
|
||
|
(done || bail)(err, tree, file);
|
||
|
});
|
||
|
|
||
|
assertDone('run', complete, done);
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Stringify a Unist node representation of a file
|
||
|
* (in string or VFile representation) into a string
|
||
|
* using the `Compiler` on the processor.
|
||
|
*
|
||
|
* @param {Node} node - Unist node.
|
||
|
* @param {(string|VFile)?} [file] - File representation.
|
||
|
* @param {Object?} [options] - Configuration.
|
||
|
* @return {string} - String representation.
|
||
|
*/
|
||
|
function stringify(node, file, options) {
|
||
|
assertConcrete('stringify');
|
||
|
assertCompiler('stringify');
|
||
|
assertNode(node);
|
||
|
|
||
|
if (!options && file && !isFile(file)) {
|
||
|
options = file;
|
||
|
file = null;
|
||
|
}
|
||
|
|
||
|
return new processor.Compiler(vfile(file), options, processor).compile(node);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*
|
||
|
* @param {(string|VFile)?} file - File representation.
|
||
|
* @param {Object?} [options] - Configuration.
|
||
|
* @param {Function?} [done] - Callback.
|
||
|
* @return {VFile} - The given or resulting VFile.
|
||
|
*/
|
||
|
function process(file, options, done) {
|
||
|
var complete = false;
|
||
|
|
||
|
assertConcrete('process');
|
||
|
assertParser('process');
|
||
|
assertCompiler('process');
|
||
|
|
||
|
if (!done && isFunction(options)) {
|
||
|
done = options;
|
||
|
options = null;
|
||
|
}
|
||
|
|
||
|
file = vfile(file);
|
||
|
|
||
|
pipeline.run(processor, {
|
||
|
file: file,
|
||
|
options: options || {}
|
||
|
}, function (err) {
|
||
|
complete = true;
|
||
|
|
||
|
if (done) {
|
||
|
done(err, file);
|
||
|
} else {
|
||
|
bail(err);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
assertDone('process', complete, done);
|
||
|
|
||
|
return file;
|
||
|
}
|
||
|
|
||
|
/* Streams. */
|
||
|
|
||
|
/**
|
||
|
* Write a chunk into memory.
|
||
|
*
|
||
|
* @param {(Buffer|string)?} chunk - Value to write.
|
||
|
* @param {string?} [encoding] - Encoding.
|
||
|
* @param {Function?} [callback] - Callback.
|
||
|
* @return {boolean} - Whether the write was succesful.
|
||
|
*/
|
||
|
function write(chunk, encoding, callback) {
|
||
|
assertConcrete('write');
|
||
|
|
||
|
if (isFunction(encoding)) {
|
||
|
callback = encoding;
|
||
|
encoding = null;
|
||
|
}
|
||
|
|
||
|
if (ended) {
|
||
|
throw new Error('Did not expect `write` after `end`');
|
||
|
}
|
||
|
|
||
|
chunks.push((chunk || '').toString(encoding || 'utf8'));
|
||
|
|
||
|
if (callback) {
|
||
|
callback();
|
||
|
}
|
||
|
|
||
|
/* Signal succesful write. */
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* End the writing. Passes all arguments to a final
|
||
|
* `write`. Starts the process, which will trigger
|
||
|
* `error`, with a fatal error, if any; `data`, with
|
||
|
* the generated document in `string` form, if
|
||
|
* succesful. If messages are triggered during the
|
||
|
* process, those are triggerd as `warning`s.
|
||
|
*
|
||
|
* @return {boolean} - Whether the last write was
|
||
|
* succesful.
|
||
|
*/
|
||
|
function end() {
|
||
|
assertConcrete('end');
|
||
|
assertParser('end');
|
||
|
assertCompiler('end');
|
||
|
|
||
|
write.apply(null, arguments);
|
||
|
|
||
|
ended = true;
|
||
|
|
||
|
process(chunks.join(''), settings, function (err, file) {
|
||
|
var messages = file.messages;
|
||
|
var length = messages.length;
|
||
|
var index = -1;
|
||
|
|
||
|
chunks = settings = null;
|
||
|
|
||
|
/* Trigger messages as warnings, except for fatal error. */
|
||
|
while (++index < length) {
|
||
|
if (messages[index] !== err) {
|
||
|
processor.emit('warning', messages[index]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (err) {
|
||
|
/* Don’t enter an infinite error throwing loop. */
|
||
|
global.setTimeout(function () {
|
||
|
processor.emit('error', err);
|
||
|
}, 4);
|
||
|
} else {
|
||
|
processor.emit('data', file.contents);
|
||
|
processor.emit('end');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Pipe the processor into a writable stream.
|
||
|
*
|
||
|
* Basically `Stream#pipe`, but inlined and
|
||
|
* simplified to keep the bundled size down.
|
||
|
*
|
||
|
* @see https://github.com/nodejs/node/blob/master/lib/stream.js#L26
|
||
|
*
|
||
|
* @param {Stream} dest - Writable stream.
|
||
|
* @param {Object?} [options] - Processing
|
||
|
* configuration.
|
||
|
* @return {Stream} - The destination stream.
|
||
|
*/
|
||
|
function pipe(dest, options) {
|
||
|
var onend = once(function () {
|
||
|
if (dest.end) {
|
||
|
dest.end();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
assertConcrete('pipe');
|
||
|
|
||
|
settings = options || {};
|
||
|
|
||
|
/**
|
||
|
* Handle data.
|
||
|
*
|
||
|
* @param {*} chunk - Data to pass through.
|
||
|
*/
|
||
|
function ondata(chunk) {
|
||
|
if (dest.writable) {
|
||
|
dest.write(chunk);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Clean listeners.
|
||
|
*/
|
||
|
function cleanup() {
|
||
|
processor.removeListener('data', ondata);
|
||
|
processor.removeListener('end', onend);
|
||
|
processor.removeListener('error', onerror);
|
||
|
processor.removeListener('end', cleanup);
|
||
|
processor.removeListener('close', cleanup);
|
||
|
|
||
|
dest.removeListener('error', onerror);
|
||
|
dest.removeListener('close', cleanup);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Close dangling pipes and handle unheard errors.
|
||
|
*
|
||
|
* @param {Error} err - Exception.
|
||
|
*/
|
||
|
function onerror(err) {
|
||
|
var handlers = processor._events.error;
|
||
|
|
||
|
cleanup();
|
||
|
|
||
|
/* Cannot use `listenerCount` in node <= 0.12. */
|
||
|
if (!handlers || !handlers.length || handlers === onerror) {
|
||
|
throw err; /* Unhandled stream error in pipe. */
|
||
|
}
|
||
|
}
|
||
|
|
||
|
processor.on('data', ondata);
|
||
|
processor.on('error', onerror);
|
||
|
processor.on('end', cleanup);
|
||
|
processor.on('close', cleanup);
|
||
|
|
||
|
/* If the 'end' option is not supplied, dest.end() will be
|
||
|
* called when the 'end' or 'close' events are received.
|
||
|
* Only dest.end() once. */
|
||
|
if (!dest._isStdio && settings.end !== false) {
|
||
|
processor.on('end', onend);
|
||
|
}
|
||
|
|
||
|
dest.on('error', onerror);
|
||
|
dest.on('close', cleanup);
|
||
|
|
||
|
dest.emit('pipe', processor);
|
||
|
|
||
|
return dest;
|
||
|
}
|
||
|
|
||
|
/* Data management. */
|
||
|
processor.data = data;
|
||
|
|
||
|
/* Lock. */
|
||
|
processor.abstract = abstract;
|
||
|
|
||
|
/* Plug-ins. */
|
||
|
processor.use = use;
|
||
|
|
||
|
/* Streaming. */
|
||
|
processor.writable = true;
|
||
|
processor.readable = true;
|
||
|
processor.write = write;
|
||
|
processor.end = end;
|
||
|
processor.pipe = pipe;
|
||
|
|
||
|
/* API. */
|
||
|
processor.parse = parse;
|
||
|
processor.stringify = stringify;
|
||
|
processor.run = run;
|
||
|
processor.process = process;
|
||
|
|
||
|
/* Expose. */
|
||
|
return processor;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if `node` is a Unist node.
|
||
|
*
|
||
|
* @param {*} node - Value.
|
||
|
* @return {boolean} - Whether `node` is a Unist node.
|
||
|
*/
|
||
|
function isNode(node) {
|
||
|
return node && typeof node.type === 'string' && node.type.length !== 0;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if `file` is a VFile.
|
||
|
*
|
||
|
* @param {*} file - Value.
|
||
|
* @return {boolean} - Whether `file` is a VFile.
|
||
|
*/
|
||
|
function isFile(file) {
|
||
|
return file && typeof file.contents === 'string';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if `fn` is a function.
|
||
|
*
|
||
|
* @param {*} fn - Value.
|
||
|
* @return {boolean} - Whether `fn` is a function.
|
||
|
*/
|
||
|
function isFunction(fn) {
|
||
|
return typeof fn === 'function';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if `compiler` is a Compiler.
|
||
|
*
|
||
|
* @param {*} compiler - Value.
|
||
|
* @return {boolean} - Whether `compiler` is a Compiler.
|
||
|
*/
|
||
|
function isCompiler(compiler) {
|
||
|
return isFunction(compiler) && compiler.prototype && isFunction(compiler.prototype.compile);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if `parser` is a Parser.
|
||
|
*
|
||
|
* @param {*} parser - Value.
|
||
|
* @return {boolean} - Whether `parser` is a Parser.
|
||
|
*/
|
||
|
function isParser(parser) {
|
||
|
return isFunction(parser) && parser.prototype && isFunction(parser.prototype.parse);
|
||
|
}
|