diff --git a/lib/helpers.js b/lib/helpers.js index 815003c..ad4cbce 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -16,6 +16,9 @@ function isObject(o) { function isString(s) { return typeof s === 'string' } +function isUndefined(u) { + return typeof u === 'undefined' +} /** * Recursively remove a directory @@ -52,6 +55,7 @@ const helpers = { isNumber, isString, isObject, + isUndefined, rm } diff --git a/lib/index.js b/lib/index.js index bee6c5a..71d945d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,7 +5,7 @@ const matter = require('gray-matter') const Mode = require('stat-mode') const path = require('path') let readdir = require('recursive-readdir') -const { rm, isString, isBoolean, isObject, isNumber } = require('./helpers') +const { rm, isString, isBoolean, isObject, isNumber, isUndefined } = require('./helpers') const thunkify = require('thunkify') const unyield = require('unyield') const utf8 = require('is-utf8') @@ -13,9 +13,69 @@ const Ware = require('ware') const match = require('micromatch') /** - * Thunks. + * @typedef {Object.} Files + */ + +/** + * Metalsmith file. Defines `mode`, `stats` and `contents` properties by default, but may be altered by plugins + * + * @typedef File + * @property {Buffer} contents - A NodeJS [buffer](https://nodejs.org/api/buffer.html) + * @property {import('fs').Stats} stats - A NodeJS [fs.Stats object](https://nodejs.org/api/fs.html#fs_class_fs_stats) + * @property {String} mode - Octal permission mode, see https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation + */ + +/** + * A callback to run when the Metalsmith build is done + * + * @callback BuildCallback + * @param {?Error} error + * @param {Object.} files + * @this {Metalsmith} + * + * @example + * function onBuildEnd(error, files) { + * if (error) throw error + * console.log('Build success') + * } + */ + +/** + * A callback to indicate that a plugin's work is done + * + * @callback DoneCallback + * @param {?Error} [error] + * + * @example + * function plugin(files, metalsmith, done) { + * // ..do stuff + * done() + * } + */ + +/** + * Metalsmith plugin + * + * @callback Plugin + * @param {Object.} files + * @param {Metalsmith} metalsmith + * @param {DoneCallback} done + * + * @example + * function drafts(files, metalsmith) { + * Object.keys(files).forEach(path => { + * if (files[path].draft) { + * delete files[path] + * } + * }) + * } + * + * metalsmith.use(drafts) */ +/** + * Thunks. + */ readdir = thunkify(readdir) /** @@ -27,7 +87,11 @@ module.exports = Metalsmith /** * Initialize a new `Metalsmith` builder with a working `directory`. * + * @callback Metalsmith * @param {String} directory + * @property {Plugin[]} plugins + * @property {String[]} ignores + * @return {Metalsmith} */ function Metalsmith(directory) { @@ -46,9 +110,13 @@ function Metalsmith(directory) { /** * Add a `plugin` function to the stack. - * - * @param {Function or Array} plugin + * @param {Plugin} plugin * @return {Metalsmith} + * + * @example + * metalsmith + * .use(drafts()) // use the drafts plugin + * .use(markdown()) // use the markdown plugin */ Metalsmith.prototype.use = function (plugin) { @@ -59,8 +127,13 @@ Metalsmith.prototype.use = function (plugin) { /** * Get or set the working `directory`. * - * @param {Object} directory - * @return {Object or Metalsmith} + * @param {Object} [directory] + * @return {String|Metalsmith} + * + * @example + * new Metalsmith('.') // set the path of the working directory through the constructor + * metalsmith.directory() // returns '.' + * metalsmith.directory('./other/path') // set the path of the working directory */ Metalsmith.prototype.directory = function (directory) { @@ -71,14 +144,18 @@ Metalsmith.prototype.directory = function (directory) { } /** - * Get or set the global `metadata` to pass to templates. + * Get or set the global `metadata`. + * + * @param {Object} [metadata] + * @return {Object|Metalsmith} * - * @param {Object} metadata - * @return {Object or Metalsmith} + * @example + * metalsmith.metadata({ sitename: 'My blog' }); // set metadata + * metalsmith.metadata() // returns { sitename: 'My blog' } */ Metalsmith.prototype.metadata = function (metadata) { - if (!arguments.length) return this._metadata + if (isUndefined(metadata)) return this._metadata assert(isObject(metadata), 'You must pass a metadata object.') this._metadata = clone(metadata) return this @@ -87,12 +164,16 @@ Metalsmith.prototype.metadata = function (metadata) { /** * Get or set the source directory. * - * @param {String} path - * @return {String or Metalsmith} + * @param {String} [path] + * @return {String|Metalsmith} + * + * @example + * metalsmith.source('./src'); // set source directory + * metalsmith.source() // returns './src' */ Metalsmith.prototype.source = function (path) { - if (!arguments.length) return this.path(this._source) + if (isUndefined(path)) return this.path(this._source) assert(isString(path), 'You must pass a source path string.') this._source = path return this @@ -101,8 +182,12 @@ Metalsmith.prototype.source = function (path) { /** * Get or set the destination directory. * - * @param {String} path - * @return {String or Metalsmith} + * @param {String} [path] + * @return {String|Metalsmith} + * + * @example + * metalsmith.destination('build'); // set destination + * metalsmith.destination() // returns 'build' */ Metalsmith.prototype.destination = function (path) { @@ -115,12 +200,16 @@ Metalsmith.prototype.destination = function (path) { /** * Get or set the maximum number of files to open at once. * - * @param {Number} max - * @return {Number or Metalsmith} + * @param {Number} [max] + * @returns {Number|Metalsmith} + * + * @example + * metalsmith.concurrency(20) // set concurrency to max 20 + * metalsmith.concurrency() // returns 20 */ Metalsmith.prototype.concurrency = function (max) { - if (!arguments.length) return this._concurrency + if (isUndefined(max)) return this._concurrency assert(isNumber(max), 'You must pass a number for concurrency.') this._concurrency = max return this @@ -129,11 +218,15 @@ Metalsmith.prototype.concurrency = function (max) { /** * Get or set whether the destination directory will be removed before writing. * - * @param {Boolean} clean - * @return {Boolean or Metalsmith} + * @param {Boolean} [clean] + * @return {Boolean|Metalsmith} + * + * @example + * metalsmith.clean(true) // clean the destination directory + * metalsmith.clean() // returns true */ Metalsmith.prototype.clean = function (clean) { - if (!arguments.length) return this._clean + if (isUndefined(clean)) return this._clean assert(isBoolean(clean), 'You must pass a boolean.') this._clean = clean return this @@ -142,12 +235,16 @@ Metalsmith.prototype.clean = function (clean) { /** * Optionally turn off frontmatter parsing. * - * @param {Boolean|Object} frontmatter - * @return {Boolean or Metalsmith} + * @param {Boolean} [frontmatter] + * @return {Boolean|Metalsmith} + * + * @example + * metalsmith.frontmatter(false) // turn off front-matter parsing + * metalsmith.frontmatter() // returns false */ Metalsmith.prototype.frontmatter = function (frontmatter) { - if (!arguments.length) return this._frontmatter + if (isUndefined(frontmatter)) return this._frontmatter assert( isBoolean(frontmatter) || isObject(frontmatter), 'You must pass a boolean or a gray-matter options object: https://github.com/jonschlinkert/gray-matter/tree/4.0.2#options' @@ -155,44 +252,52 @@ Metalsmith.prototype.frontmatter = function (frontmatter) { this._frontmatter = frontmatter return this } + /** - * Add a file or files to the list of ignores. + * Get or set the list of filepaths or glob patterns to ignore * - * @param {String or Strings} The names of files or directories to ignore. - * @return {Metalsmith} + * @method Metalsmith#ignore + * @param {String|String[]} [files] - The names or glob patterns of files or directories to ignore. + * @return {Metalsmith|String[]} + * + * @example + * metalsmith.ignore() + * metalsmith.ignore('layouts') // ignore the layouts directory + * metalsmith.ignore(['.*', 'data.json']) // ignore dot files & a data file */ Metalsmith.prototype.ignore = function (files) { - if (!arguments.length) return this.ignores.slice() + if (isUndefined(files)) return this.ignores.slice() this.ignores = this.ignores.concat(files) return this } /** - * Resolve `paths` relative to the root directory. + * Resolve `paths` relative to the metalsmith `directory`. * - * @param {String} paths... + * @param {...string} paths * @return {String} + * + * @example + * metalsmith.path('./path','to/file.ext') */ -Metalsmith.prototype.path = function () { - const paths = [].slice.call(arguments) - paths.unshift(this.directory()) - return path.resolve.apply(path, paths) +Metalsmith.prototype.path = function (...paths) { + return path.resolve.apply(path, [this.directory(), ...paths]) } /** * Match filepaths in the source directory by [glob](https://en.wikipedia.org/wiki/Glob_(programming)) pattern. - * Use the third arg `input` to match against a subset of strings/ filepaths + * If `input` is not specified, patterns are matched against `Object.keys(files)` * * @param {String|String[]} patterns - one or more glob patterns - * @param {import('micromatch').Options} options - [micromatch options](https://github.com/micromatch/micromatch#options) - * @param {String[]} [input=Object.keys(files)] array of strings to match against + * @param {import('micromatch').Options} options - [micromatch options](https://github.com/micromatch/micromatch#options), except `format` + * @param {String[]} [input] array of strings to match against * @returns {String[]} An array of matching file paths */ Metalsmith.prototype.match = function (patterns, options, input) { input = input || Object.keys(this._files) - if (!this._files) return [] + if (!(input && input.length)) return [] options = Object.assign({ dot: true }, options || {}, { // required to convert forward to backslashes on Windows and match the file keys properly format: path.normalize @@ -203,8 +308,16 @@ Metalsmith.prototype.match = function (patterns, options, input) { /** * Build with the current settings to the destination directory. * - * @param {Function} callback - * @return {Promise} + * @param {BuildCallback} [callback] + * @return {Promise} + * @fulfills {Files} + * @rejects {Error} + * + * @example + * metalsmith.build(function(error, files) { + * if (error) throw error + * console.log('Build success!') + * }) */ Metalsmith.prototype.build = function (callback) { @@ -248,7 +361,16 @@ Metalsmith.prototype.build = function (callback) { /** * Process files through plugins without writing out files. * - * @return {Object} + * @method Metalsmith#process + * @param {BuildCallback} [callback] + * @return {Object.} + * + * @example + * metalsmith.process(err => { + * if (err) throw err + * console.log('Success') + * console.log(this.metadata()) + * }) */ Metalsmith.prototype.process = unyield(function* () { @@ -260,8 +382,10 @@ Metalsmith.prototype.process = unyield(function* () { /** * Run a set of `files` through the plugins stack. * - * @param {Object} files - * @param {Array} plugins + * @method Metalsmith#run + * @package + * @param {Object.} files + * @param {Plugin[]} plugins * @return {Object} */ @@ -276,7 +400,9 @@ Metalsmith.prototype.run = unyield(function* (files, plugins) { * 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) + * @method Metalsmith#read + * @package + * @param {String} [dir] * @return {Object} */ @@ -312,8 +438,10 @@ Metalsmith.prototype.read = unyield(function* (dir) { * Read a `file` by path. If the path is not absolute, it will be resolved * relative to the source directory. * + * @method Metalsmith#readFile + * @package * @param {String} file - * @return {Object} + * @returns {File} */ Metalsmith.prototype.readFile = unyield(function* (file) { @@ -361,8 +489,10 @@ Metalsmith.prototype.readFile = unyield(function* (file) { * 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) + * @method Metalsmith#write + * @package + * @param {Object.} files + * @param {String} [dir] */ Metalsmith.prototype.write = unyield(function* (files, dir) { @@ -389,8 +519,10 @@ Metalsmith.prototype.write = unyield(function* (files, dir) { * Write a `file` by path with `data`. If the path is not absolute, it will be * resolved relative to the destination directory. * + * @method Metalsmith#writeFile + * @package * @param {String} file - * @param {Object} data + * @param {File} data */ Metalsmith.prototype.writeFile = unyield(function* (file, data) { diff --git a/test/index.js b/test/index.js index 316f81c..56652b0 100644 --- a/test/index.js +++ b/test/index.js @@ -285,8 +285,6 @@ describe('Metalsmith', function () { }) }) - // this test is included because micromatch has an obscure option called "windows", - // which is set to true by default on Windows. Maintains compat with how Metalsmith 2.x functions it('should not transform backslashes to forward slashes in the returned matches', function (done) { const m = Metalsmith(fixture('match')) m.use(function windowsPaths(files) {