461 lines
10 KiB
JavaScript
461 lines
10 KiB
JavaScript
|
'use strict'
|
|||
|
|
|||
|
var bail = require('bail')
|
|||
|
var buffer = require('is-buffer')
|
|||
|
var extend = require('extend')
|
|||
|
var plain = require('is-plain-obj')
|
|||
|
var trough = require('trough')
|
|||
|
var vfile = require('vfile')
|
|||
|
|
|||
|
// 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(error, tree, file) {
|
|||
|
if (error) {
|
|||
|
next(error)
|
|||
|
} else {
|
|||
|
ctx.tree = tree
|
|||
|
ctx.file = file
|
|||
|
next()
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function pipelineStringify(p, ctx) {
|
|||
|
var result = p.stringify(ctx.tree, ctx.file)
|
|||
|
|
|||
|
if (result === undefined || result === null) {
|
|||
|
// Empty.
|
|||
|
} else if (typeof result === 'string' || buffer(result)) {
|
|||
|
if ('value' in ctx.file) {
|
|||
|
ctx.file.value = result
|
|||
|
}
|
|||
|
|
|||
|
ctx.file.contents = result
|
|||
|
} else {
|
|||
|
ctx.file.result = result
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Function to create the first processor.
|
|||
|
function unified() {
|
|||
|
var attachers = []
|
|||
|
var transformers = trough()
|
|||
|
var namespace = {}
|
|||
|
var freezeIndex = -1
|
|||
|
var frozen
|
|||
|
|
|||
|
// Data management.
|
|||
|
processor.data = data
|
|||
|
|
|||
|
// Lock.
|
|||
|
processor.freeze = freeze
|
|||
|
|
|||
|
// Plugins.
|
|||
|
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 index = -1
|
|||
|
|
|||
|
while (++index < attachers.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.
|
|||
|
// Plugins 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 transformer
|
|||
|
|
|||
|
if (frozen) {
|
|||
|
return processor
|
|||
|
}
|
|||
|
|
|||
|
while (++freezeIndex < attachers.length) {
|
|||
|
values = attachers[freezeIndex]
|
|||
|
|
|||
|
if (values[1] === false) {
|
|||
|
continue
|
|||
|
}
|
|||
|
|
|||
|
if (values[1] === true) {
|
|||
|
values[1] = undefined
|
|||
|
}
|
|||
|
|
|||
|
transformer = values[0].apply(processor, values.slice(1))
|
|||
|
|
|||
|
if (typeof transformer === 'function') {
|
|||
|
transformers.use(transformer)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
frozen = true
|
|||
|
freezeIndex = Infinity
|
|||
|
|
|||
|
return processor
|
|||
|
}
|
|||
|
|
|||
|
// Data management.
|
|||
|
// Getter / setter for processor-specific informtion.
|
|||
|
function data(key, value) {
|
|||
|
if (typeof key === 'string') {
|
|||
|
// 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
|
|||
|
}
|
|||
|
|
|||
|
// Plugin 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 (typeof value === 'function') {
|
|||
|
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 (typeof value === 'function') {
|
|||
|
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 index = -1
|
|||
|
|
|||
|
if (plugins === null || plugins === undefined) {
|
|||
|
// Empty.
|
|||
|
} else if (typeof plugins === 'object' && 'length' in plugins) {
|
|||
|
while (++index < plugins.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(true, entry[1], value)
|
|||
|
}
|
|||
|
|
|||
|
entry[1] = value
|
|||
|
} else {
|
|||
|
attachers.push(slice.call(arguments))
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function find(plugin) {
|
|||
|
var index = -1
|
|||
|
|
|||
|
while (++index < attachers.length) {
|
|||
|
if (attachers[index][0] === plugin) {
|
|||
|
return attachers[index]
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 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, 'parse')) {
|
|||
|
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 && typeof file === 'function') {
|
|||
|
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(error, tree, file) {
|
|||
|
tree = tree || node
|
|||
|
if (error) {
|
|||
|
reject(error)
|
|||
|
} 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 result
|
|||
|
var complete
|
|||
|
|
|||
|
run(node, file, done)
|
|||
|
|
|||
|
assertDone('runSync', 'run', complete)
|
|||
|
|
|||
|
return result
|
|||
|
|
|||
|
function done(error, tree) {
|
|||
|
complete = true
|
|||
|
result = tree
|
|||
|
bail(error)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 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, 'compile')) {
|
|||
|
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(error) {
|
|||
|
if (error) {
|
|||
|
reject(error)
|
|||
|
} else if (resolve) {
|
|||
|
resolve(file)
|
|||
|
} else {
|
|||
|
cb(null, file)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Process the given document (in string or vfile representation), sync.
|
|||
|
function processSync(doc) {
|
|||
|
var file
|
|||
|
var complete
|
|||
|
|
|||
|
freeze()
|
|||
|
assertParser('processSync', processor.Parser)
|
|||
|
assertCompiler('processSync', processor.Compiler)
|
|||
|
file = vfile(doc)
|
|||
|
|
|||
|
process(file, done)
|
|||
|
|
|||
|
assertDone('processSync', 'process', complete)
|
|||
|
|
|||
|
return file
|
|||
|
|
|||
|
function done(error) {
|
|||
|
complete = true
|
|||
|
bail(error)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Check if `value` is a constructor.
|
|||
|
function newable(value, name) {
|
|||
|
return (
|
|||
|
typeof value === 'function' &&
|
|||
|
value.prototype &&
|
|||
|
// A function with keys in its prototype is probably a constructor.
|
|||
|
// Classes’ prototype methods are not enumerable, so we check if some value
|
|||
|
// exists in the prototype.
|
|||
|
(keys(value.prototype) || name in 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 (typeof Parser !== 'function') {
|
|||
|
throw new Error('Cannot `' + name + '` without `Parser`')
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Assert a compiler is available.
|
|||
|
function assertCompiler(name, Compiler) {
|
|||
|
if (typeof Compiler !== 'function') {
|
|||
|
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.\nCreate a new processor first, by invoking it: use `processor()` instead of `processor`.'
|
|||
|
)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Assert `node` is a unist node.
|
|||
|
function assertNode(node) {
|
|||
|
if (!node || typeof node.type !== 'string') {
|
|||
|
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'
|
|||
|
)
|
|||
|
}
|
|||
|
}
|