hexo
Version:
A fast, simple & powerful blog framework, powered by Node.js.
281 lines (241 loc) • 5.81 kB
JavaScript
var fs = require('graceful-fs'),
pathFn = require('path'),
async = require('async'),
_ = require('lodash'),
chokidar = require('chokidar'),
EventEmitter = require('events').EventEmitter,
Pattern = require('./pattern'),
HexoError = require('../error'),
util = require('../util'),
file = util.file2,
File = require('./file');
/**
* This module is used to manage files and processors.
*
* @class Box
* @module hexo
* @constructor
* @param {String} base
* @param {Object} [options] See [chokidar](https://github.com/paulmillr/chokidar)
* @param {Boolean} [options.presistent=true]
* @param {RegExp} [options.ignored=/[\/\\]\./]
* @param {Boolean} [options.ignoreInitial=true]
* @extends EventEmitter
*/
var Box = module.exports = function Box(base, options){
/**
* Base path.
*
* @property base
* @type {String}
*/
this.base = base;
/**
* Processor collection.
*
* @property processors
* @type {Array}
*/
this.processors = [];
/**
* Processing files.
*
* @property processingFiles
* @type {Object}
* @private
*/
this.processingFiles = {};
/**
* A instance of watcher.
*
* @property watcher
* @type {FSWatcher}
*/
this.watcher = null;
/**
* Indicates if the box is processing.
*
* @property isProcessing
* @type {Boolean}
* @private
*/
this.isProcessing = false;
/**
* @property options
* @type Object
*/
this.options = _.extend({
presistent: true,
ignored: /[\/\\]\./,
ignoreInitial: true
}, options);
};
Box.prototype.__proto__ = EventEmitter.prototype;
/**
* Adds a processor to the box.
*
* @method addProcessor
* @param {RegExp|String} pattern The path pattern of the processor. See {% crosslink Box.Pattern %} for more info.
* @param {Function} fn The processor function.
*/
Box.prototype.addProcessor = function(pattern, fn){
this.processors.push({
pattern: new Pattern(pattern),
process: fn
});
};
/**
* Dispatches files to processors.
*
* @method _dispatch
* @param {String} type Available types: create, update, delete
* @param {String} path File path
* @param {Function} [callback]
* @private
* @async
*/
Box.prototype._dispatch = function(type, path, callback){
if (typeof callback !== 'function') callback = function(){};
var self = this;
async.each(this.processors, function(processor, next){
if (!processor.pattern.test(path)) return next();
var params = processor.pattern.match(path),
src = pathFn.join(self.base, path);
processor.process(new File(self, src, path, type, params), function(err){
if (err){
if (err instanceof HexoError){
return next(err);
} else {
return next(HexoError.wrap(err, 'Process failed: ' + path));
}
}
next();
});
}, function(err){
if (err) return callback(err);
self.processingFiles[path] = false;
hexo.log.d('Processed: %s', path);
callback();
});
};
/**
* Loads all files and runs processors.
*
* @method process
* @param {Array|String} [files] Files to be processed
* @param {Function} [callback]
* @async
*/
Box.prototype.process = function(files, callback){
if (!callback){
if (typeof files === 'function'){
callback = files;
files = null;
} else {
callback = function(){};
}
}
if (this.isProcessing) return callback(new Error('Box is processing!'));
var self = this,
base = this.base;
this.isProcessing = true;
/**
* Fired before the process started.
*
* @event processBefore
* @param {String} base The base path of the box
* @for Hexo
*/
hexo.emit('processBefore', base);
async.waterfall([
function(next){
if (files){
if (!Array.isArray(files)) files = [files];
next(null, files);
} else {
file.list(base, next);
}
},
function(files, next){
async.each(files, function(item, next){
var type, path;
if (_.isObject(item)){
path = item.path;
type = item.type;
} else {
path = item;
type = 'update';
}
// Skip the processing file
if (self.processingFiles[path]) return;
self.processingFiles[path] = true;
self._dispatch(type, path, next);
}, next);
}
], function(err){
self.isProcessing = false;
/**
* Fired after the process has been done.
*
* @event processAfter
* @param {String} base The base path of the box
* @for Hexo
*/
hexo.emit('processAfter', base);
callback(err);
});
};
var chokidarEventMap = {
add: 'create',
change: 'update',
unlink: 'delete'
};
/**
* Starts watching.
*
* @method watch
* @for Box
*/
Box.prototype.watch = function(){
if (this.watcher) throw new Error('Watcher has already started.');
var self = this,
queue = [],
isRunning = false,
timer;
var timerFn = function(){
if (queue.length && !isRunning){
isRunning = true;
self.process(queue, function(err){
isRunning = false;
queue.length = 0;
if (err) return hexo.log.e(err);
});
}
};
this.watcher = chokidar.watch(this.base, this.options)
.on('all', function(event, src){
var type = chokidarEventMap[event],
path = src.substring(self.base.length);
if (!type) return;
if (timer) clearTimeout(timer);
queue.push({
type: type,
path: path
});
timer = setTimeout(timerFn, 100);
hexo.log.log(type, src);
})
.on('error', function(err){
self.emit('error', err);
});
};
/**
* Stops watching.
*
* @method unwatch
*/
Box.prototype.unwatch = function(){
if (!this.watcher) throw new Error('Watcher hasn\'t started yet.');
this.watcher.stop();
this.watcher = null;
};