345 lines
8.1 KiB
JavaScript
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;
|
||
|
}
|
||
|
});
|