Website/node_modules/metalsmith/lib/index.js
2015-12-02 18:21:44 -05:00

345 lines
8.1 KiB
JavaScript

var absolute = require('absolute');
var assert = require('assert');
var clone = require('clone');
var fs = require('co-fs-extra');
var is = require('is');
var matter = require('gray-matter');
var Mode = require('stat-mode');
var path = require('path');
var readdir = require('recursive-readdir');
var rm = require('rimraf');
var thunkify = require('thunkify');
var unyield = require('unyield');
var utf8 = require('is-utf8');
var Ware = require('ware');
/**
* Thunks.
*/
readdir = thunkify(readdir);
rm = thunkify(rm);
/**
* Export `Metalsmith`.
*/
module.exports = Metalsmith;
/**
* Initialize a new `Metalsmith` builder with a working `directory`.
*
* @param {String} directory
*/
function Metalsmith(directory){
if (!(this instanceof Metalsmith)) return new Metalsmith(directory);
assert(directory, 'You must pass a working directory path.');
this.plugins = [];
this.ignores = [];
this.directory(directory);
this.metadata({});
this.source('src');
this.destination('build');
this.concurrency(Infinity);
this.clean(true);
this.frontmatter(true);
}
/**
* Add a `plugin` function to the stack.
*
* @param {Function or Array} plugin
* @return {Metalsmith}
*/
Metalsmith.prototype.use = function(plugin){
this.plugins.push(plugin);
return this;
};
/**
* Get or set the working `directory`.
*
* @param {Object} directory
* @return {Object or Metalsmith}
*/
Metalsmith.prototype.directory = function(directory){
if (!arguments.length) return path.resolve(this._directory);
assert(is.string(directory), 'You must pass a directory path string.');
this._directory = directory;
return this;
};
/**
* Get or set the global `metadata` to pass to templates.
*
* @param {Object} metadata
* @return {Object or Metalsmith}
*/
Metalsmith.prototype.metadata = function(metadata){
if (!arguments.length) return this._metadata;
assert(is.object(metadata), 'You must pass a metadata object.');
this._metadata = clone(metadata);
return this;
};
/**
* Get or set the source directory.
*
* @param {String} path
* @return {String or Metalsmith}
*/
Metalsmith.prototype.source = function(path){
if (!arguments.length) return this.path(this._source);
assert(is.string(path), 'You must pass a source path string.');
this._source = path;
return this;
};
/**
* Get or set the destination directory.
*
* @param {String} path
* @return {String or Metalsmith}
*/
Metalsmith.prototype.destination = function(path){
if (!arguments.length) return this.path(this._destination);
assert(is.string(path), 'You must pass a destination path string.');
this._destination = path;
return this;
};
/**
* Get or set the maximum number of files to open at once.
*
* @param {Number} max
* @return {Number or Metalsmith}
*/
Metalsmith.prototype.concurrency = function(max){
if (!arguments.length) return this._concurrency;
assert(is.number(max), 'You must pass a number for concurrency.');
this._concurrency = max;
return this;
};
/**
* Get or set whether the destination directory will be removed before writing.
*
* @param {Boolean} clean
* @return {Boolean or Metalsmith}
*/
Metalsmith.prototype.clean = function(clean){
if (!arguments.length) return this._clean;
assert(is.boolean(clean), 'You must pass a boolean.');
this._clean = clean;
return this;
};
/**
* Optionally turn off frontmatter parsing.
*
* @param {Boolean} frontmatter
* @return {Boolean or Metalsmith}
*/
Metalsmith.prototype.frontmatter = function(frontmatter){
if (!arguments.length) return this._frontmatter;
assert(is.boolean(frontmatter), 'You must pass a boolean.');
this._frontmatter = frontmatter;
return this;
};
/**
* Add a file or files to the list of ignores.
*
* @param {String or Strings} The names of files or directories to ignore.
* @return {Metalsmith}
*/
Metalsmith.prototype.ignore = function(files){
if (!arguments.length) return this.ignores.slice();
this.ignores = this.ignores.concat(files);
return this;
};
/**
* Resolve `paths` relative to the root directory.
*
* @param {String} paths...
* @return {String}
*/
Metalsmith.prototype.path = function(){
var paths = [].slice.call(arguments);
paths.unshift(this.directory());
return path.resolve.apply(path, paths);
};
/**
* Build with the current settings to the destination directory.
*
* @return {Object}
*/
Metalsmith.prototype.build = unyield(function*(){
var clean = this.clean();
var dest = this.destination();
if (clean) yield rm(dest);
var files = yield this.read();
files = yield this.run(files);
yield this.write(files);
return files;
});
/**
* Run a set of `files` through the plugins stack.
*
* @param {Object} files
* @param {Array} plugins
* @return {Object}
*/
Metalsmith.prototype.run = unyield(function*(files, plugins){
var ware = new Ware(plugins || this.plugins);
var run = thunkify(ware.run.bind(ware));
var res = yield run(files, this);
return res[0];
});
/**
* Read a dictionary of files from a `dir`, parsing frontmatter. If no directory
* is provided, it will default to the source directory.
*
* @param {String} dir (optional)
* @return {Object}
*/
Metalsmith.prototype.read = unyield(function*(dir){
dir = dir || this.source();
var read = this.readFile.bind(this);
var concurrency = this.concurrency();
var ignores = this.ignores || null;
var paths = yield readdir(dir, ignores);
var files = [];
var complete = 0;
var batch;
while (complete < paths.length) {
batch = paths.slice(complete, complete + concurrency);
batch = yield batch.map(read);
files = files.concat(batch);
complete += concurrency;
}
return paths.reduce(memoizer, {});
function memoizer(memo, file, i) {
file = path.relative(dir, file);
memo[file] = files[i];
return memo;
}
});
/**
* Read a `file` by path. If the path is not absolute, it will be resolved
* relative to the source directory.
*
* @param {String} file
* @return {Object}
*/
Metalsmith.prototype.readFile = unyield(function*(file){
var src = this.source();
var ret = {};
if (!absolute(file)) file = path.resolve(src, file);
try {
var frontmatter = this.frontmatter();
var stats = yield fs.stat(file);
var buffer = yield fs.readFile(file);
var parsed;
if (frontmatter && utf8(buffer)) {
try {
parsed = matter(buffer.toString());
} catch (e) {
var err = new Error('Invalid frontmatter in the file at: ' + file);
err.code = 'invalid_frontmatter';
throw err;
}
ret = parsed.data;
ret.contents = new Buffer(parsed.content);
} else {
ret.contents = buffer;
}
ret.mode = Mode(stats).toOctal();
ret.stats = stats;
} catch (e) {
if (e.code == 'invalid_frontmatter') throw e;
e.message = 'Failed to read the file at: ' + file + '\n\n' + e.message;
e.code = 'failed_read';
throw e;
}
return ret;
});
/**
* Write a dictionary of `files` to a destination `dir`. If no directory is
* provided, it will default to the destination directory.
*
* @param {Object} files
* @param {String} dir (optional)
*/
Metalsmith.prototype.write = unyield(function*(files, dir){
dir = dir || this.destination();
var write = this.writeFile.bind(this);
var concurrency = this.concurrency();
var keys = Object.keys(files);
var complete = 0;
var batch;
while (complete < keys.length) {
batch = keys.slice(complete, complete + concurrency);
yield batch.map(writer);
complete += concurrency;
}
function writer(key){
var file = path.resolve(dir, key);
return write(file, files[key]);
}
});
/**
* Write a `file` by path with `data`. If the path is not absolute, it will be
* resolved relative to the destination directory.
*
* @param {String} file
* @param {Object} data
*/
Metalsmith.prototype.writeFile = unyield(function*(file, data){
var dest = this.destination();
if (!absolute(file)) file = path.resolve(dest, file);
try {
yield fs.outputFile(file, data.contents);
if (data.mode) yield fs.chmod(file, data.mode);
} catch (e) {
e.message = 'Failed to write the file at: ' + file + '\n\n' + e.message;
throw e;
}
});