docpad
Version:
DocPad is a dynamic static site generator. Write your content as files, or import your content from other external sources. Render the content with plugins. And deploy your static or dynamic website to your favourite hosting provider.
1,626 lines (1,525 loc) • 176 kB
JavaScript
// Generated by CoffeeScript 2.5.1
//#*
// The central module for DocPad
// @module DocPad
//#
// =====================================
// Requires
// Standard
/**
* Contains methods for managing the DocPad application.
* Extends https://github.com/bevry/event-emitter-grouped
*
* You can use it like so:
*
* new DocPad(docpadConfig, function(err, docpad) {
* if (err) return docpad.fatal(err)
* return docpad.action(action, function(err) {
* if (err) return docpad.fatal(err)
* return console.log('OK')
* })
* })
*
* @class Docpad
* @constructor
* @extends EventEmitterGrouped
*/
var BasePlugin, CSON, Collection, DocPad, DocumentModel, ElementsCollection, Errlop, EventEmitterGrouped, Events, FileModel, FilesCollection, Filter, Human, Logger, MetaCollection, Model, PluginLoader, Progress, QueryCollection, ScriptsCollection, StylesCollection, TaskGroup, ambi, ansiStyles, balUtil, docpadUtil, eachr, envFile, extendr, extractOptsAndCallback, fetch, fsUtil, ignorefs, isTruthy, isUser, pathUtil, pick, queryEngine, rimraf, safefs, safeps, scandir, typeChecker, unbounded, union, uniq, util,
hasProp = {}.hasOwnProperty,
boundMethodCheck = function(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new Error('Bound instance method accessed before binding'); } };
util = require('util');
pathUtil = require('path');
// External
({Logger, Human, Filter} = require('caterpillar'));
fsUtil = require('fs');
Errlop = require('errlop').default;
queryEngine = require('query-engine');
({uniq, union, pick} = require('underscore'));
CSON = require('cson');
balUtil = require('bal-util');
scandir = require('scandirectory');
extendr = require('extendr');
eachr = require('eachr');
typeChecker = require('typechecker');
ambi = require('ambi');
unbounded = require('unbounded');
({TaskGroup} = require('taskgroup'));
safefs = require('safefs');
safeps = require('safeps');
ignorefs = require('ignorefs');
rimraf = require('rimraf');
Progress = require('progress-title');
fetch = require('node-fetch');
extractOptsAndCallback = require('extract-opts');
({EventEmitterGrouped} = require('event-emitter-grouped'));
envFile = require('envfile');
ansiStyles = require('ansistyles');
// Base
({Events, Model, Collection, QueryCollection} = require('./base'));
// Utils
docpadUtil = require('./util');
// Models
FileModel = require('./models/file');
DocumentModel = require('./models/document');
// Collections
FilesCollection = require('./collections/files');
ElementsCollection = require('./collections/elements');
MetaCollection = require('./collections/meta');
ScriptsCollection = require('./collections/scripts');
StylesCollection = require('./collections/styles');
// Plugins
PluginLoader = require('@bevry/pluginloader').default;
BasePlugin = require('docpad-baseplugin');
// ---------------------------------
// Variables
isUser = docpadUtil.isUser();
isTruthy = function(i) {
return Boolean(i);
};
DocPad = (function() {
class DocPad extends EventEmitterGrouped {
// Libraries
// Here for legacy API reasons
//@DocPad: DocPad
//@Backbone: require('backbone')
//@queryEngine: queryEngine
// Allow for `DocPad.create()` as an alias for `new DocPad()`
static create(...args) {
return new this(...args);
}
// Require a local DocPad file
// Before v6.73.0 this allowed requiring of files inside src/lib, as well as files inside src
// After v6.73.0 it only allows requiring of files inside src/lib as that makes more sense
// After v6.80.9 it only allows requiring specific aliases
static require(name) {
if (name === 'testers') {
console.log(`'
docpad.require('testers') is deprecated, replacement instructions at: https://github.com/docpad/docpad-plugintester`);
return require('docpad-plugintester');
} else {
throw new Errlop("docpad.require is limited to requiring: testers");
}
}
/**
* Get the DocPad version number
* @method getVersion
* @return {Number}
*/
getVersion() {
if (this.version == null) {
this.version = require(this.packagePath).version;
}
return this.version;
}
/**
* Get the DocPad version string
* @method getVersionString
* @return {String}
*/
getVersionString() {
if (docpadUtil.isLocalDocPadExecutable()) {
return util.format(this.getLocale().versionLocal, this.getVersion(), this.corePath);
} else {
return util.format(this.getLocale().versionGlobal, this.getVersion(), this.corePath);
}
}
// Process getters
/**
* Get the process platform
* @method getProcessPlatform
* @return {Object}
*/
getProcessPlatform() {
return process.platform;
}
/**
* Get the process version string
* @method getProcessVersion
* @return {String}
*/
getProcessVersion() {
return process.version.replace(/^v/, '');
}
/**
* Get the caterpillar logger instance bound to DocPad
* @method getLogger
* @return {Object} caterpillar logger
*/
getLogger() {
return this.logger;
}
/**
* Destructor. Destroy the caterpillar logger instances bound to DocPad
* @private
* @method {Object} destroyLoggers
*/
destroyLoggers() {
// @logger.end()
this.logger = null;
return this;
}
/**
* Create a timer and add it to the known timers
* @method timer
* @param {string} type - either timeout or interval
* @param {number} time - the time to apply to the timer
* @param {method} method - the method to use for the timer
*/
timer(id, type, time, method) {
var timer;
if (this.timers == null) {
this.timers = {};
}
// Create a new timer
if (type != null) {
this.timer(id); // clear
if (type === 'timeout') {
if (time === -1) {
timer = setImmediate(method);
} else {
timer = setTimeout(method, time);
}
} else if (type === 'interval') {
timer = setInterval(method, time);
} else {
throw new Errlop('unexpected type on new timer');
}
this.timers[id] = {id, type, time, method, timer};
// Destroy an old timer
} else if (this.timers[id]) {
if (this.timers[id].type === 'interval') {
clearInterval(this.timers[id].timer);
} else if (this.timers[id].type === 'timeout') {
if (this.timers[id].time === -1) {
if (typeof clearImmediate === "function") {
clearImmediate(this.timers[id].timer);
}
} else {
clearTimeout(this.timers[id].timer);
}
} else {
throw new Errlop('unexpected type on stored timer');
}
this.timers[id] = null;
}
return this;
}
/**
* Destructor. Destroy all the timers we have kept.
* @private
* @method {Object} destroyTimers
*/
destroyTimers(timer) {
var key, ref, value;
if (this.timers == null) {
this.timers = {};
}
ref = this.timers;
for (key in ref) {
if (!hasProp.call(ref, key)) continue;
value = ref[key];
this.timer(key);
}
return this;
}
/**
* Update the configuration of the progress instance, to either enable it or disable it
* Progress will be enabled if DocPad config 'progress' is true
* @private
* @method updateProgress
* @param {boolean} [enabled] manually enable or disable the progress bar
*/
updateProgress(enabled) {
var config, debug, docpad, options;
// Prepare
docpad = this;
config = docpad.getConfig();
debug = this.getDebugging();
// Enabled
if (enabled == null) {
enabled = config.progress;
}
// If we are in debug mode, then output more detailed title messages
options = {};
if (debug) {
options.verbose = true;
options.interval = 0;
}
// options.log = true
// If we wish to have it enabled
if (enabled) {
if (this.progressInstance) {
this.progressInstance.pause().configure(options).resume();
} else {
this.progressInstance = Progress.create(options).start();
}
} else if (this.progressInstance) {
this.progressInstance.stop().configure(options);
}
// Return
return this;
}
/**
* Get the action runner instance bound to docpad
* @method getActionRunner
* @return {Object} the action runner instance
*/
getActionRunner() {
return this.actionRunnerInstance;
}
/**
* Apply the passed DocPad action arguments
* @method {Object} action
* @param {Object} args
* @return {Object}
*/
action(action, opts, next) {
var locale, ref;
// Prepare
[opts, next] = extractOptsAndCallback(opts, next);
locale = this.getLocale();
// Log
if ((ref = this.progressInstance) != null) {
ref.resume();
}
this.log('debug', util.format(locale.actionStart, action));
// Act
docpadUtil.action.call(this, action, opts, (...args) => {
var err, ref1;
// Prepare
err = args[0];
// Log
if ((ref1 = this.progressInstance) != null) {
ref1.stop();
}
if (err) {
this.error(new Errlop(util.format(locale.actionFailure, action), err));
} else {
this.log('debug', util.format(locale.actionSuccess, action));
}
return typeof next === "function" ? next(...args) : void 0;
});
return this;
}
/**
* Get the list of available events
* @method getEvents
* @return {Object} string array of event names
*/
getEvents() {
return this.events;
}
/**
* Description for getDatabase
* @method {Object} getDatabase
*/
getDatabase() {
return this.database;
}
/**
* Destructor. Destroy the DocPad database
* @private
* @method destroyDatabase
*/
destroyDatabase() {
if (this.database != null) {
this.database.destroy();
this.database = null;
}
return this;
}
/* {
* A collection of meta elements
meta: null # Elements Collection
* A collection of script elements
scripts: null # Scripts Collection
* Collection of style elements
styles: null # Styles Collection
} */
/**
* Get a block by block name. Optionally clone block.
* @method getBlock
* @param {String} name
* @param {Object} [clone]
* @return {Object} block
*/
getBlock(name, clone) {
var block, classname;
block = this.blocks[name];
if (clone) {
classname = name[0].toUpperCase() + name.slice(1) + 'Collection';
block = new this[classname](block.models);
}
return block;
}
/**
* Set a block by name and value
* @method setBlock
* @param {String} name
* @param {Object} value
*/
setBlock(name, value) {
if (this.blocks[name] != null) {
this.blocks[name].destroy();
if (value) {
this.blocks[name] = value;
} else {
delete this.blocks[name];
}
} else {
this.blocks[name] = value;
}
return this;
}
/**
* Get all blocks
* @method getBlocks
* @return {Object} collection of blocks
*/
getBlocks() {
return this.blocks;
}
/**
* Set all blocks
* @method setBlocks
* @param {Object} blocks
*/
setBlocks(blocks) {
var name, value;
for (name in blocks) {
if (!hasProp.call(blocks, name)) continue;
value = blocks[name];
this.setBlock(name, value);
}
return this;
}
/**
* Apply the passed function to each block
* @method eachBlock
* @param {Function} fn
*/
eachBlock(fn) {
eachr(this.blocks, fn);
return this;
}
/**
* Destructor. Destroy all blocks
* @private
* @method destroyBlocks
*/
destroyBlocks() {
var block, name, ref;
if (this.blocks) {
ref = this.blocks;
for (name in ref) {
if (!hasProp.call(ref, name)) continue;
block = ref[name];
block.destroy();
this.blocks[name] = null;
}
}
return this;
}
/**
* Get a collection by collection name or key.
* This is often accessed within the docpad.coffee
* file or a layout/page via @getCollection.
* Because getCollection returns a docpad collection,
* a call to this method is often chained with a
* QueryEngine style query.
*
* @getCollection('documents').findAllLive({relativeOutDirPath: 'posts'},[{date:-1}])
*
* @method getCollection
* @param {String} value
* @return {Object} collection
*/
getCollection(value) {
var collection, j, k, len, len1, ref, ref1;
if (value) {
if (typeof value === 'string') {
if (value === 'database') {
return this.getDatabase();
} else {
ref = this.collections;
for (j = 0, len = ref.length; j < len; j++) {
collection = ref[j];
if (value === collection.options.name || value === collection.options.key) {
return collection;
}
}
}
} else {
ref1 = this.collections;
for (k = 0, len1 = ref1.length; k < len1; k++) {
collection = ref1[k];
if (value === collection) {
return collection;
}
}
}
}
return null;
}
/**
* Destroy a collection by collection name or key
* @method destroyCollection
* @param {String} value
* @return {Object} description
*/
destroyCollection(value) {
if (value) {
if (typeof value === 'string' && value !== 'database') {
this.collections = this.collections.filter(function(collection) {
if (value === collection.options.name || value === collection.options.key) {
if (collection != null) {
collection.destroy();
}
return false;
} else {
return true;
}
});
} else if (value !== this.getDatabase()) {
this.collections = this.collections.filter(function(collection) {
if (value === collection) {
if (collection != null) {
collection.destroy();
}
return false;
} else {
return true;
}
});
}
}
return null;
}
/**
* Add a collection
* @method addCollection
* @param {Object} collection
*/
addCollection(collection) {
if (collection && (collection !== this.getDatabase() && collection !== this.getCollection(collection))) {
this.collections.push(collection);
}
return this;
}
/**
* Set a name for a collection.
* A collection can have multiple names
*
* The partials plugin (https://github.com/docpad/docpad-plugin-partials)
* creates a live collection and passes this to setCollection with
* the name 'partials'.
*
* # Add our partials collection
* docpad.setCollection('partials', database.createLiveChildCollection()
* .setQuery('isPartial', {
* $or:
* isPartial: true
* fullPath: $startsWith: config.partialsPath
* })
* .on('add', (model) ->
* docpad.log('debug', util.format(locale.addingPartial, model.getFilePath()))
* model.setDefaults(
* isPartial: true
* render: false
* write: false
* )
* )
* )
*
*
* @method setCollection
* @param {String} name the name to give to the collection
* @param {Object} collection a DocPad collection
*/
setCollection(name, collection) {
if (collection) {
if (name) {
collection.options.name = name;
if (this.getCollection(name) !== collection) {
this.destroyCollection(name);
}
}
return this.addCollection(collection);
} else {
return this.destroyCollection(name);
}
}
/**
* Get the DocPad project's collections
* @method getCollections
* @return {Object} the collections
*/
getCollections() {
return this.collections;
}
/**
* Set the DocPad project's collections
* @method setCollections
*/
setCollections(collections) {
var j, len, name, value;
if (Array.isArray(collections)) {
for (j = 0, len = collections.length; j < len; j++) {
value = collections[j];
this.addCollection(value);
}
} else {
for (name in collections) {
if (!hasProp.call(collections, name)) continue;
value = collections[name];
this.setCollection(name, value);
}
}
return this;
}
/**
* Apply the passed function to each collection
* @method eachCollection
* @param {Function} fn
*/
eachCollection(fn) {
var collection, index, j, len, ref;
fn(this.getDatabase(), 'database');
ref = this.collections;
for (index = j = 0, len = ref.length; j < len; index = ++j) {
collection = ref[index];
fn(collection, collection.options.name || collection.options.key || index);
}
return this;
}
/**
* Destructor. Destroy the DocPad project's collections.
* @private
* @method destroyCollections
*/
destroyCollections() {
var collection, j, len, ref;
if (this.collections) {
ref = this.collections;
for (j = 0, len = ref.length; j < len; j++) {
collection = ref[j];
collection.destroy();
}
this.collections = [];
}
return this;
}
// ---------------------------------
// Collection Helpers
/**
* Get all the files in the DocPad database (will use live collections)
* @method getFiles
* @param {Object} query
* @param {Object} sorting
* @param {Object} paging
* @return {Object} collection
*/
getFiles(query, sorting, paging) {
var collection, key;
key = JSON.stringify({query, sorting, paging});
collection = this.getCollection(key);
if (!collection) {
collection = this.getDatabase().findAllLive(query, sorting, paging);
collection.options.key = key;
this.addCollection(collection);
}
return collection;
}
/**
* Get a single file based on a query
* @method getFile
* @param {Object} query
* @param {Object} sorting
* @param {Object} paging
* @return {Object} a file
*/
getFile(query, sorting, paging) {
var file;
file = this.getDatabase().findOne(query, sorting, paging);
return file;
}
/**
* Get files at a path
* @method getFilesAtPath
* @param {String} path
* @param {Object} sorting
* @param {Object} paging
* @return {Object} files
*/
getFilesAtPath(path, sorting, paging) {
var files, query;
query = {
$or: [
{
relativePath: {
$startsWith: path
}
},
{
fullPath: {
$startsWith: path
}
}
]
};
files = this.getFiles(query, sorting, paging);
return files;
}
/**
* Get a file at a relative or absolute path or url
* @method getFileAtPath
* @param {String} path
* @param {Object} sorting
* @param {Object} paging
* @return {Object} a file
*/
getFileAtPath(path, sorting, paging) {
var file;
file = this.getDatabase().fuzzyFindOne(path, sorting, paging);
return file;
}
/**
* Get a file by its id
* @method getFileById
* @param {String} id
* @param {Object} [opts={}]
* @return {Object} a file
*/
getFileById(id, opts = {}) {
var file;
if (opts.collection == null) {
opts.collection = this.getDatabase();
}
file = opts.collection.get(id);
return file;
}
/**
* Remove the query string from a url
* Pathname convention taken from document.location.pathname
* @method getUrlPathname
* @param {String} url
* @return {String}
*/
getUrlPathname(url) {
return url.replace(/\?.*/, '');
}
/**
* Get a file by its selector (this is used to fetch layouts by their name)
* @method getFileBySelector
* @param {Object} selector
* @param {Object} [opts={}]
* @return {Object} a file
*/
getFileBySelector(selector, opts = {}) {
var file;
if (opts.collection == null) {
opts.collection = this.getDatabase();
}
file = opts.collection.fuzzyFindOne(selector);
return file;
}
/**
* Get Complete Template Data
* @method getTemplateData
* @param {Object} userTemplateData
* @return {Object} templateData
*/
getTemplateData(userTemplateData) {
var base, base1, base2, docpad, locale, templateData;
// Prepare
userTemplateData || (userTemplateData = {});
docpad = this;
locale = this.getLocale();
// Set the initial docpad template data
if (this.initialTemplateData == null) {
this.initialTemplateData = {
// Site Properties
site: {},
// Environment
getEnvironment: function() {
return docpad.getEnvironment();
},
// Environments
getEnvironments: function() {
return docpad.getEnvironments();
},
// Set that we reference other files
referencesOthers: function(flag) {
var document;
document = this.getDocument();
document.referencesOthers();
return null;
},
// Get the Document
getDocument: function() {
return this.documentModel;
},
// Get a Path in respect to the current document
getPath: function(path, parentPath) {
var document;
document = this.getDocument();
path = document.getPath(path, parentPath);
return path;
},
// Get Files
getFiles: function(query, sorting, paging) {
var result;
this.referencesOthers();
result = docpad.getFiles(query, sorting, paging);
return result;
},
// Get another file's URL based on a relative path
getFile: function(query, sorting, paging) {
var result;
this.referencesOthers();
result = docpad.getFile(query, sorting, paging);
return result;
},
// Get Files At Path
getFilesAtPath: function(path, sorting, paging) {
var result;
this.referencesOthers();
path = this.getPath(path);
result = docpad.getFilesAtPath(path, sorting, paging);
return result;
},
// Get another file's model based on a relative path
getFileAtPath: function(relativePath) {
var path, result;
this.referencesOthers();
path = this.getPath(relativePath);
result = docpad.getFileAtPath(path);
return result;
},
// Get a specific file by its id
getFileById: function(id) {
var result;
this.referencesOthers();
result = docpad.getFileById(id);
return result;
},
// Get the entire database
getDatabase: function() {
this.referencesOthers();
return docpad.getDatabase();
},
// Get a pre-defined collection
getCollection: function(name) {
this.referencesOthers();
return docpad.getCollection(name);
},
// Get a block
getBlock: function(name) {
return docpad.getBlock(name, true);
},
// Include another file taking in a relative path
include: function(subRelativePath, strict = true) {
var err, file;
file = this.getFileAtPath(subRelativePath);
if (file) {
if (strict && file.get('rendered') === false) {
if (docpad.getConfig().renderPasses === 1) {
docpad.warn(util.format(locale.renderedEarlyViaInclude, subRelativePath));
}
return null;
}
return file.getOutContent();
} else {
err = new Errlop(util.format(locale.includeFailed, subRelativePath));
throw err;
}
}
};
}
// Fetch our result template data
templateData = extendr.extend({}, this.initialTemplateData, this.pluginsTemplateData, this.getConfig().templateData, userTemplateData);
// Add site data
(base = templateData.site).url || (base.url = '');
(base1 = templateData.site).date || (base1.date = new Date());
(base2 = templateData.site).keywords || (base2.keywords = []);
if (typeChecker.isString(templateData.site.keywords)) {
templateData.site.keywords = templateData.site.keywords.split(/,\s*/g);
}
// Return
return templateData;
}
/**
* Get the locale (language code and locale code)
* @method getLocale
* @return {Object} locale
*/
getLocale(key) {
var err, localeError, locales;
if (this.locale == null) {
try {
locales = this.getPath('locales').map(function(locale) {
return require(locale);
});
this.locale = extendr.extend(...locales);
} catch (error1) {
localeError = error1;
docpad.warn(new Errlop('Failed to load a locale', localeError));
try {
this.locale = require(this.getPath('locale'));
} catch (error1) {
err = error1;
docpad.fatal(new Errlop('Failed to load any locale', err));
this.locale = {};
}
}
}
if (key) {
return this.locale[key] || key;
} else {
return this.locale;
}
}
// -----------------------------
// Environments
/**
* Get the DocPad environment, eg: development,
* production or static
* @method getEnvironment
* @return {String} the environment
*/
getEnvironment() {
return this.env;
}
/**
* Get the environments
* @method getEnvironments
* @return {Array} array of environment strings
*/
getEnvironments() {
return this.envs;
}
/**
* Get the DocPad configuration
* @method getConfig
* @return {Object} the DocPad configuration object
*/
getConfig() {
return this.config || {};
}
// =================================
// Initialization Functions
/**
* Create our own custom TaskGroup instance for DocPad.
* That will listen to tasks as they execute and provide debugging information.
* @method createTaskGroup
* @param {Object} opts
* @return {TaskGroup}
*/
createTaskGroup(...opts) {
var docpad, progress, tasks;
boundMethodCheck(this, DocPad);
docpad = this;
progress = docpad.progressInstance;
tasks = TaskGroup.create(...opts);
// Listen to executing tasks and output their progress
tasks.on('running', function() {
var config, name, totals;
config = tasks.getConfig();
name = tasks.getNames();
if (progress) {
totals = tasks.getItemTotals();
return progress.update(name, totals);
} else {
return docpad.log('debug', name + ' > running');
}
});
// Listen to executing tasks and output their progress
tasks.on('item.add', function(item) {
var config, name;
config = tasks.getConfig();
name = item.getNames();
if (!progress) {
docpad.log('debug', name + ' > added');
}
// Listen to executing tasks and output their progress
item.on('started', function(item) {
var totals;
config = tasks.getConfig();
name = item.getNames();
if (progress) {
totals = tasks.getItemTotals();
return progress.update(name, totals);
} else {
return docpad.log('debug', name + ' > started');
}
});
// Listen to executing tasks and output their progress
return item.done(function(err) {
var totals;
config = tasks.getConfig();
name = item.getNames();
if (progress) {
totals = tasks.getItemTotals();
return progress.update(name, totals);
} else {
return docpad.log('debug', name + ' > done');
}
});
});
// Return
return tasks;
}
/**
* Constructor method. Sets up the DocPad instance.
* next(err)
* @method constructor
* @param {Object} instanceConfig
* @param {Function} next callback
* @param {Error} next.err
* @param {DocPad} next.docpad
*/
constructor(instanceConfig, next) {
var color, configEventContext, docpad, filter, j, len, lineLevel, logPath, logger, methodName, ref;
super();
this.createTaskGroup = this.createTaskGroup.bind(this);
[instanceConfig, next] = extractOptsAndCallback(instanceConfig, next);
docpad = this;
// Allow DocPad to have unlimited event listeners
this.setMaxListeners(0);
ref = "action log warn error fatal inspect notify checkRequest activeHandles onBeforeExit onSignalInterruptOne onSignalInterruptTwo onSignalInterruptThree destroyWatchers".split(/\s+/);
// Binders
// Using this over coffescript's => on class methods, ensures that the method length is kept
for (j = 0, len = ref.length; j < len; j++) {
methodName = ref[j];
this[methodName] = this[methodName].bind(this);
}
// Adjust configPaths
if (typeChecker.isString(instanceConfig.configPaths)) {
instanceConfig.configPaths = [instanceConfig.configPaths];
}
// Dereference and initialise advanced variables
// we deliberately ommit initialTemplateData here, as it is setup in getTemplateData
this.slowPlugins = {};
this.loadedPlugins = {};
this.pluginsTemplateData = {};
this.collections = [];
this.blocks = {};
this.websitePackageConfig = {};
this.websiteConfig = {};
this.userConfig = {};
this.initialConfig = extendr.dereferenceJSON(this.initialConfig);
this.instanceConfig = instanceConfig || {};
this.config = this.mergeConfigs();
// Prepare the loggers
if (instanceConfig.logLevel == null) {
instanceConfig.logLevel = this.initialConfig.logLevel;
}
color = instanceConfig.color;
lineLevel = -1;
if (instanceConfig.silent) {
instanceConfig.logLevel = 3; // 3:error, 2:critical, 1:alert, 0:emergency
instanceConfig.progress = instanceConfig.welcome = false;
}
if (instanceConfig.verbose || instanceConfig.debug) {
instanceConfig.logLevel = 7;
lineLevel = 7;
}
// Create the loggers
logger = new Logger({
lineLevel: lineLevel
});
filter = new Filter({
filterLevel: instanceConfig.logLevel
});
// Apply the loggers
this.logger = logger;
// Console
logger.pipe(filter).pipe(new Human({
color: color
})).pipe(process.stdout);
// File
if (instanceConfig.debug) {
logPath = this.getPath(false, 'log');
safefs.unlink(logPath, function() {
return logger.pipe(new Human({
color: false
})).pipe(fsUtil.createWriteStream(logPath));
});
}
// Forward log events to the logger
this.on('log', function(...args) {
return docpad.log.apply(this, args);
});
// Setup configuration event wrappers
configEventContext = {docpad}; // here to allow the config event context to persist between event calls
this.getEvents().forEach(function(eventName) {
// Bind to the event
return docpad.on(eventName, function(opts, next) {
var args, eventHandler, ref1;
eventHandler = (ref1 = docpad.getConfig().events) != null ? ref1[eventName] : void 0;
// Fire the config event handler for this event, if it exists
if (typeChecker.isFunction(eventHandler)) {
args = [opts, next];
return ambi(unbounded.binder.call(eventHandler, configEventContext), ...args);
} else {
// It doesn't exist, so lets continue
return next();
}
});
});
// Create our action runner
this.actionRunnerInstance = this.createTaskGroup('action runner', {
abortOnError: false,
destroyOnceDone: false
}).whenDone(function(err) {
var ref1;
if ((ref1 = docpad.progressInstance) != null) {
ref1.update('');
}
if (err) {
return docpad.error(err);
}
});
// Setup the database
this.database = new FilesCollection(null, {
name: 'database'
}).on('remove', function(model, options) {
var outPath, updatedModels;
// Skip if we are not a writeable file
if (model.get('write') === false) {
return;
}
// Ensure we regenerate anything (on the next regeneration) that was using the same outPath
outPath = model.get('outPath');
if (outPath) {
updatedModels = docpad.database.findAll({outPath});
updatedModels.remove(model);
if (updatedModels.length) {
updatedModels.each(function(model) {
return model.set({
'mtime': new Date()
});
});
docpad.log('info', 'Updated mtime for these models due to the removal of a similar one:', updatedModels.pluck('relativePath'));
}
}
// Return safely
return true;
}).on('add change:outPath', function(model) {
var existingModels, outPath, previousModels, previousOutPath;
// Skip if we are not a writeable file
if (model.get('write') === false) {
return;
}
// Prepare
outPath = model.get('outPath');
previousOutPath = model.previous('outPath');
// Check if we have changed our outPath
if (previousOutPath) {
// Ensure we regenerate anything (on the next regeneration) that was using the same outPath
previousModels = docpad.database.findAll({
outPath: previousOutPath
});
previousModels.remove(model);
if (previousModels.length) {
previousModels.each(function(previousModel) {
return previousModel.set({
'mtime': new Date()
});
});
docpad.log('info', 'Updated mtime for these models due to the addition of a similar one:', previousModels.pluck('relativePath'));
}
}
// Determine if there are any conflicts with the new outPath
if (outPath) {
existingModels = docpad.database.findAll({outPath});
existingModels.each(function(existingModel) {
var existingModelPath, modelPath;
if (existingModel.id !== model.id) {
modelPath = model.get('fullPath') || (model.get('relativePath') + ':' + model.id);
existingModelPath = existingModel.get('fullPath') || (existingModel.get('relativePath') + ':' + existingModel.id);
return docpad.warn(util.format(docpad.getLocale().outPathConflict, outPath, modelPath, existingModelPath));
}
});
}
// Return safely
return true;
});
// Continue with load and ready
this.action('load ready', {}, function(err) {
if (next != null) {
return next(err, docpad);
} else if (err) {
return docpad.fatal(err);
}
});
// Chain
this;
}
/**
* Destructor. Destroy the DocPad instance
* This is an action, and should be called as such
* E.g. docpad.action('destroy', next)
* @method destroy
* @param {Object} opts
* @param {Function} next
* @param {Error} next.err
*/
destroy(opts, next) {
var config, docpad, dropped, locale;
if (this.destroying) {
return this;
}
this.destroying = true;
// Prepare
[opts, next] = extractOptsAndCallback(opts, next);
docpad = this;
config = this.getConfig();
locale = this.getLocale();
// Log
docpad.log('info', locale.destroyDocPad);
// Drop all the remaining tasks
dropped = this.getActionRunner().clearRemaining();
if (dropped) {
docpad.error(`DocPad destruction had to drop ${Number(dropped)} action tasks`);
}
// Destroy Timers
docpad.destroyTimers();
// Wait a configurable oment
docpad.timer('destroy', 'timeout', config.destroyDelay, function() {
// Destroy Plugins
return docpad.emitSerial('docpadDestroy', function(eventError) {
var err, finalError;
// Check
if (eventError) {
// Note
err = new Errlop("DocPad's destroyEvent event failed", eventError);
docpad.fatal(err);
return typeof next === "function" ? next(err) : void 0;
}
try {
// Destroy Timers
// Final closures and checks
docpad.destroyTimers();
// Destroy Plugins
docpad.destroyPlugins();
// Destroy Watchers
docpad.destroyWatchers();
// Destroy Blocks
docpad.destroyBlocks();
// Destroy Collections
docpad.destroyCollections();
// Destroy Database
docpad.destroyDatabase();
// Destroy progress
docpad.updateProgress(false);
// Destroy Logging
docpad.destroyLoggers();
// Destroy Process Listeners
process.removeListener('uncaughtException', docpad.fatal);
process.removeListener('uncaughtException', docpad.error);
process.removeListener('beforeExit', docpad.onBeforeExit);
process.removeListener('SIGINT', docpad.onSignalInterruptOne);
process.removeListener('SIGINT', docpad.onSignalInterruptTwo);
process.removeListener('SIGINT', docpad.onSignalInterruptThree);
// Destroy DocPad Listeners
docpad.removeAllListeners();
} catch (error1) {
finalError = error1;
// Note
err = new Errlop("DocPad's final destruction efforts failed", finalError);
docpad.fatal(err);
return typeof next === "function" ? next(err) : void 0;
}
// Success
docpad.log(locale.destroyedDocPad); // log level omitted, as this will hit console.log
return typeof next === "function" ? next() : void 0;
});
});
return this;
}
/**
* Emit event, serial
* @private
* @method emitSerial
* @param {String} eventName
* @param {Object} opts
* @param {Function} next
* @param {Error} next.err
*/
emitSerial(eventName, opts, next) {
var docpad, locale;
// Prepare
[opts, next] = extractOptsAndCallback(opts, next);
docpad = this;
locale = docpad.getLocale();
// Log
docpad.log('debug', util.format(locale.emittingEvent, eventName));
// Emit
super.emitSerial(eventName, opts, function(err) {
if (err) {
// Check
return next(err);
}
// Log
docpad.log('debug', util.format(locale.emittedEvent, eventName));
// Forward
return next(err);
});
return this;
}
/**
* Emit event, parallel
* @private
* @method emitParallel
* @param {String} eventName
* @param {Object} opts
* @param {Function} next
* @param {Error} next.err
*/
emitParallel(eventName, opts, next) {
var docpad, locale;
// Prepare
[opts, next] = extractOptsAndCallback(opts, next);
docpad = this;
locale = docpad.getLocale();
// Log
docpad.log('debug', util.format(locale.emittingEvent, eventName));
// Emit
super.emitParallel(eventName, opts, function(err) {
if (err) {
// Check
return next(err);
}
// Log
docpad.log('debug', util.format(locale.emittedEvent, eventName));
// Forward
return next(err);
});
return this;
}
// =================================
// Helpers
/**
* Get the ignore options for the DocPad project
* @method getIgnoreOpts
* @return {Array} string array of ignore options
*/
getIgnoreOpts() {
return pick(this.config, ['ignorePaths', 'ignoreHiddenFiles', 'ignoreCommonPatterns', 'ignoreCustomPatterns']);
}
/**
* Is the supplied path ignored?
* @method isIgnoredPath
* @param {String} path
* @param {Object} [opts={}]
* @return {Boolean}
*/
isIgnoredPath(path, opts = {}) {
opts = extendr.extend(this.getIgnoreOpts(), opts);
return ignorefs.isIgnoredPath(path, opts);
}
/**
* Scan directory
* @method scandir
* @param {Object} [opts={}]
*/
//NB: How does this work? What is returned?
//Does it require a callback (next) passed as
//one of the options
scandir(opts = {}) {
opts = extendr.extend(this.getIgnoreOpts(), opts);
return scandir(opts);
}
/**
* Watch Directory. Wrapper around the Bevry watchr
* module (https://github.com/bevry/watchr). Used
* internally by DocPad to watch project documents
* and files and then activate the regeneration process
* when any of those items are updated.
* @private
* @method watchdir
* @param {String} path - the path to watch
* @param {Object} listeners - listeners to attach to the watcher
* @param {Function} next - completion callback accepting error
* @return {Object} the watcher
*/
watchdir(path, listeners, next) {
var key, opts, stalker, value;
opts = extendr.extend(this.getIgnoreOpts(), this.config.watchOptions || {});
stalker = require('watchr').create(path);
for (key in listeners) {
if (!hasProp.call(listeners, key)) continue;
value = listeners[key];
stalker.on(key, value);
}
stalker.setConfig(opts);
stalker.watch(next);
return stalker;
}
/**
* Watch Directories. Wrapper around watchdir.
* @private
* @method watchdirs
* @param {Array} paths - the paths to watch
* @param {Object} listeners - listeners to attach to the watcher
* @param {Function} next - completion callback accepting error and watchers/stalkers
*/
watchdirs(paths, listeners, next) {
var docpad, stalkers, tasks;
docpad = this;
stalkers = [];
tasks = new TaskGroup('watching directories').setConfig({
concurrency: 0
}).done(function(err) {
var j, len, stalker;
if (err) {
for (j = 0, len = stalkers.length; j < len; j++) {
stalker = stalkers[j];
stalker.close();
}
return next(err);
} else {
return next(err, stalkers);
}
});
paths.forEach(function(path) {
return tasks.addTask(`watching ${path}`, function(done) {
// check if the dir exists first as reloadPaths may not apparently
return safefs.exists(path, function(exists) {
if (!exists) {
return done();
}
return stalkers.push(docpad.watchdir(path, listeners, done));
});
});
});
tasks.run();
return this;
}
// =================================
// Setup and Loading
/**
* DocPad is ready. Peforms the tasks needed after DocPad construction
* and DocPad has loaded. Triggers the docpadReady event.
* next(err,docpadInstance)
* @private
* @method ready
* @param {Object} [opts]
* @param {Function} next
* @param {Error} next.err
* @param {Object} next.docpadInstance
*/
ready(opts, next) {
var config, docpad, instanceConfig, locale, pluginsList, tasks;
// Prepare
[instanceConfig, next] = extractOptsAndCallback(instanceConfig, next);
docpad = this;
config = this.getConfig();
locale = this.getLocale();
// Render Single Extensions
this.DocumentModel.prototype.defaults.renderSingleExtensions = config.renderSingleExtensions;
// Fetch the plugins
pluginsList = Object.keys(this.loadedPlugins).sort().join(', ');
// Welcome Output
docpad.log('info', util.format(locale.welcome, this.getVersionString()));
docpad.log('notice', locale.welcomeDonate);
docpad.log('info', locale.welcomeContribute);
docpad.log('info', util.format(locale.welcomePlugins, pluginsList));
docpad.log('info', util.format(locale.welcomeEnvironment, this.getEnvironment()));
// Prepare
tasks = this.createTaskGroup('ready tasks').done(function(err) {
if (err) {
// Error?
return docpad.error(err);
}
return typeof next === "function" ? next(null, docpad) : void 0;
});
// kept here in case plugins use it
tasks.addTask('welcome event', function(complete) {
if (!config.welcome) {
// No welcome
return complete();
}
// Welcome
return docpad.emitSerial('welcome', {docpad}, complete);
});
tasks.addTask('emit docpadReady', function(complete) {
return docpad.emitSerial('docpadReady', {docpad}, complete);
});
// Run tasks
tasks.run();
return this;
}
/**
* Performs the merging of the passed configuration objects
* @private
* @method mergeConfigs
*/
mergeConfigs(configPackages, destination = {}) {
var configPackage, configsToMerge, env, envConfig, j, k, len, len1, ref, ref1;
// A plugin is calling us with its configuration
if (!configPackages) {
// Apply the environment
// websitePackageConfig.env is left out of the detection here as it is usually an object
// that is already merged with our process.env by the environment runner
// rather than a string which is the docpad convention
this.env = this.instanceConfig.env || this.websiteConfig.env || this.initialConfig.env || process.env.NODE_ENV || 'development';
this.envs = this.env.split(/[, ]+/);
// Merge the configurations together
configPackages = [this.initialConfig, this.userConfig, this.websiteConfig, this.instanceConfig];
}
// Figure out merging
configsToMerge = [destination];
for (j = 0, len = configPackages.length; j < len; j++) {
configPackage = configPackages[j];
if (!configPackage) {
continue;
}
configsToMerge.push(configPackage);
ref = this.envs;
for (k = 0, len1 = ref.length; k < len1; k++) {
env = ref[k];
envConfig = (ref1 = configPackage.environments) != null ? ref1[env] : void 0;
if (envConfig) {
configsToMerge.push(envConfig);
}
}
}
// Merge
return extendr.deep(...configsToMerge);
}
/**
* Legacy version of mergeConmergeConfigsfigurations
* @private
* @method mergeConfigurations
*/
mergeConfigurations(configPackages, [destination]) {
return this.mergeConfigs(configPackages, destination);
}
/**
* Set the DocPad configuration object.
* Performs a number of tasks, including
* merging the pass instanceConfig with DocPad's
* other config objects.
* next(err,config)
* @private
* @method setConfig
* @param {Object} instanceConfig
* @param {Object} next
* @param {Error} next.err
* @param {Object} next.config
*/
setConfig(instanceConfig) {
var docpad, locale, next;
// Prepare
[instanceConfig, next] = extractOptsAndCallback(instanceConfig, next);
docpad = this;
locale = this.getLocale();
if (instanceConfig) {
// Apply the instance configuration, generally we won't have it at this level
// as it would have been applied earlier the load step
extendr.deepDefaults(this.instanceConfig, instanceConfig);
}
// Merge the configurations together
this.config = this.mergeConfig