Skip to content

Commit

Permalink
Add plugin reconfiguring
Browse files Browse the repository at this point in the history
Closes GH-29.

Previously, plugins could be added multiple times.  This behaviour
is kind-of funky, no matter how it’s dealt with.  The best way to
deal with it though, I think, is by merging the configuration for
those double attached plugins and only invoking the plugin once.
With the merged config.

This problem will become more apparent due to presets.

Take the following code:

```js
unified().use(toc, {maxDepth: 2}).use(toc, {tight: true})
```

...this is now handled the same as:

```js
unified().use(toc, {maxDepth: 2, tight: true})
```

Additionally, this renames `abstract` to `freeze`, to better signal
what it represents.

Finally, plugins are no longer invoked when (first) used. Instead,
they are called upon when actually used (when the processor is frozen,
either explicitly through `freeze()` or implicitly when first `parse`,
`stringify`, `run`, or `process` is called).
  • Loading branch information
wooorm committed Feb 20, 2017
1 parent fe746a7 commit e73da90
Show file tree
Hide file tree
Showing 8 changed files with 541 additions and 414 deletions.
225 changes: 136 additions & 89 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ var vfile = require('vfile');
var trough = require('trough');
var string = require('x-is-string');
var func = require('x-is-function');
var array = require('isarray');

/* Expose an abstract processor. */
module.exports = unified().abstract();
/* Expose a frozen processor. */
module.exports = unified().freeze();

/* Methods. */
var slice = [].slice;

/* Process pipeline. */
Expand Down Expand Up @@ -40,13 +40,13 @@ function unified() {
var attachers = [];
var transformers = trough();
var namespace = {};
var concrete = true;
var frozen = false;

/* Data management. */
processor.data = data;

/* Lock. */
processor.abstract = abstract;
processor.freeze = freeze;

/* Plug-ins. */
processor.attachers = attachers;
Expand Down Expand Up @@ -79,29 +79,61 @@ function unified() {
return destination;
}

/* Abstract: used to signal an abstract processor which
* should made concrete before using.
/* Freeze: used to signal a processor that has finished
* configuration.
*
* For example, take unified itself. It’s abstract.
* For example, take unified itself. It’s frozen.
* Plug-ins should not be added to it. Rather, it should
* be made concrete (by invoking it) before modifying it.
* be extended, by invoking it, before modifying it.
*
* In essence, always invoke this when exporting a
* processor. */
function abstract() {
concrete = false;
function freeze() {
var length = attachers.length;
var index = -1;
var values;
var plugin;
var options;
var transformer;

if (frozen) {
return processor;
}

while (++index < length) {
values = attachers[index];
plugin = values[0];
options = values[1];
transformer = null;

if (options === false) {
return;
}

if (options === true) {
values[1] = undefined;
}

transformer = plugin.apply(processor, values.slice(1));

if (func(transformer)) {
transformers.use(transformer);
}
}

frozen = true;

return processor;
}

/* Data management.
* Getter / setter for processor-specific informtion. */
function data(key, value) {
assertConcrete('data', concrete);

if (string(key)) {
/* Set `key`. */
if (arguments.length === 2) {
assertUnfrozen('data', frozen);

namespace[key] = value;

return processor;
Expand All @@ -111,15 +143,15 @@ function unified() {
return (has(namespace, key) && namespace[key]) || null;
}

/* Get space. */
if (!key) {
return namespace;
}

/* Set space. */
namespace = key;
if (key) {
assertUnfrozen('data', frozen);
namespace = key;
return processor;
}

return processor;
/* Get space. */
return namespace;
}

/* Plug-in management.
Expand All @@ -130,78 +162,100 @@ function unified() {
* * a tuple of one attacher and options.
* * a matrix: list containing any of the above and
* matrices.
* * a processor: another processor to use all its
* plugins (except parser if there’s already one).
* * a preset: an object with `plugins` (any of the
* above, optional), and `settings` (object, optional). */
function use(value) {
var args = slice.call(arguments, 0);
var params = args.slice(1);
var parser;
var index;
var length;
var transformer;
var result;
var settings;

assertConcrete('use', concrete);
assertUnfrozen('use', frozen);

if (!func(value)) {
/* Multiple attachers. */
if (value === null || value === undefined) {
/* empty */
} else if (func(value)) {
addPlugin.apply(null, arguments);
} else if (typeof value === 'object') {
if ('length' in value) {
index = -1;
length = value.length;

if (!func(value[0])) {
/* Matrix of things. */
while (++index < length) {
use(value[index]);
}
} else if (func(value[1])) {
/* List of things. */
while (++index < length) {
use.apply(null, [value[index]].concat(params));
}
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 {
/* Arguments. */
use.apply(null, value);
addPreset(value);
}

return processor;
} else {
throw new Error('Expected usable value, not `' + value + '`');
}
}

/* Preset. */
use(value.plugins || []);
namespace.settings = extend(namespace.settings || {}, value.settings || {});
function addList(plugins) {
var length;
var index;

return processor;
if (plugins === null || plugins === undefined) {
/* empty */
} else if (array(plugins)) {
length = plugins.length;
index = -1;

while (++index < length) {
add(plugins[index]);
}
} else {
throw new Error('Expected a list of plugins, not `' + plugins + '`');
}
}

/* Store attacher. */
attachers.push(args);
function addPlugin(plugin, value) {
var entry = find(plugin);

/* Use a processor (except its parser if there’s already one.
* Note that the processor is stored on `attachers`, making
* it possibly mutating in the future, but also ensuring
* the parser isn’t overwritten in the future either. */
if (proc(value)) {
parser = processor.Parser;
result = use(value.attachers);
if (entry) {
if (value !== false && entry[1] !== false && !array(value)) {
value = extend(entry[1], value);
}

if (parser) {
processor.Parser = parser;
entry[1] = value;
} else {
attachers.push(slice.call(arguments));
}

return result;
}
}

function find(plugin) {
var length = attachers.length;
var index = -1;
var entry;

/* Single attacher. */
transformer = value.apply(processor, params);
while (++index < length) {
entry = attachers[index];

if (func(transformer)) {
transformers.use(transformer);
if (entry[0] === plugin) {
return entry;
}
}

return processor;
}

/* Parse a file (in string or VFile representation)
Expand All @@ -211,7 +265,7 @@ function unified() {
var Parser = processor.Parser;
var file = vfile(doc);

assertConcrete('parse', concrete);
freeze();
assertParser('parse', Parser);

if (newable(Parser)) {
Expand All @@ -224,8 +278,8 @@ function unified() {
/* Run transforms on a Unist node representation of a file
* (in string or VFile representation), async. */
function run(node, file, cb) {
assertConcrete('run', concrete);
assertNode(node);
freeze();

if (!cb && func(file)) {
cb = file;
Expand Down Expand Up @@ -260,8 +314,6 @@ function unified() {
var complete = false;
var result;

assertConcrete('runSync', concrete);

run(node, file, done);

assertDone('runSync', 'run', complete);
Expand All @@ -282,7 +334,7 @@ function unified() {
var Compiler = processor.Compiler;
var file = vfile(doc);

assertConcrete('stringify', concrete);
freeze();
assertCompiler('stringify', Compiler);
assertNode(node);

Expand All @@ -299,7 +351,7 @@ function unified() {
* resulting node using the `Compiler` on the processor,
* and store that result on the VFile. */
function process(doc, cb) {
assertConcrete('process', concrete);
freeze();
assertParser('process', processor.Parser);
assertCompiler('process', processor.Compiler);

Expand Down Expand Up @@ -332,7 +384,7 @@ function unified() {
var complete = false;
var file;

assertConcrete('processSync', concrete);
freeze();
assertParser('processSync', processor.Parser);
assertCompiler('processSync', processor.Compiler);
file = vfile(doc);
Expand All @@ -350,11 +402,6 @@ function unified() {
}
}

/* Check if `processor` is a unified processor. */
function proc(processor) {
return func(processor) && func(processor.use) && func(processor.process);
}

/* Check if `func` is a constructor. */
function newable(value) {
return func(value) && keys(value.prototype);
Expand Down Expand Up @@ -383,12 +430,12 @@ function assertCompiler(name, Compiler) {
}
}

/* Assert the processor is concrete. */
function assertConcrete(name, concrete) {
if (!concrete) {
/* Assert the processor is not frozen. */
function assertUnfrozen(name, frozen) {
if (frozen) {
throw new Error(
'Cannot invoke `' + name + '` on abstract processor.\n' +
'To make the processor concrete, invoke it: ' +
'Cannot invoke `' + name + '` on a frozen processor.\n' +
'Create a new processor first, by invoking it: ' +
'use `processor()` instead of `processor`.'
);
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"bail": "^1.0.0",
"extend": "^3.0.0",
"has": "^1.0.1",
"isarray": "^2.0.1",
"trough": "^1.0.0",
"vfile": "^2.0.0",
"x-is-function": "^1.0.4",
Expand Down

0 comments on commit e73da90

Please sign in to comment.