ajs
Version:
Asynchronous templating in Node.js
523 lines (449 loc) • 16.9 kB
JavaScript
"use strict";
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var fs = require('fs'),
path = require('path'),
util = require('./util'),
EventEmitter = require('events').EventEmitter,
Compiler = require('./compiler'),
Cache = require('./cache'),
streamData = require("stream-data");
/**
* Template
* Creates a new `Template` instance by receiving the AJS source
* and wrapping it in a function that gives it a new
* VM to run in on each call.
*
* @name Template
* @function
* @param {String} source The template source.
* @param {Object} opts The compile options.
* @returns {Function} The template function.
*/
function Template(source, opts) {
var fn = new Compiler(source, opts).compile();
return function (locals, cb) {
return new VM(fn, opts).render(locals, cb);
};
}
/**
* loadInclude
* When we include templates from a running VM, we specificy the `bare` option
* so the compiler doesn't wrap the template in a new VM.
*
* @name loadInclude
* @function
* @param {String} filename The filename to include.
* @param {Objec} opts The compile options.
* @returns {Template} The new template.
*/
Template.loadInclude = function (filename, opts) {
opts = opts || {};
opts.bare = true;
var template = null,
cached = null,
cache = typeof opts.cache != 'undefined' ? opts.cache : true;
try {
if (cache && (cached = Cache.getSync(filename))) {
return cached;
} else {
opts.filename = filename;
template = new Compiler(fs.readFileSync(filename, 'utf8'), opts).compile();
Cache.set(filename, template);
return template;
}
} catch (e) {
e.message = "In " + filename + ", " + e.message;
throw e;
}
};
var VM = function (_EventEmitter) {
_inherits(VM, _EventEmitter);
/**
* VM
* This is the AJS Virtual Machine.
* In the VM we execute the compiled AJS code in a context that captures
* and buffers output until callbacks return and we're ready to flush.
*
* @name VM
* @function
* @param {Function} func The function returned by the compiler.
* @param {Object} opts An object containing the following fields:
*
* - `filename` (String): The path to the ajs template.
*
* @returns {VM} The `VM` instance.
*/
function VM(func, opts) {
_classCallCheck(this, VM);
var _this = _possibleConstructorReturn(this, (VM.__proto__ || Object.getPrototypeOf(VM)).call(this));
_this.filename = util.resolveFilename(opts.filename);
_this.dirname = util.resolveDirname(_this.filename);
_this.line = 1;
_this.error = null;
_this._function = func;
_this._locals = null;
_this._vmContext_ = opts._vmContext_;
if (_this._vmContext_) {
_this._vmContext_.inc = _this._include.bind(_this);
_this._vmContext_.modInclude = _this._moduleInclude.bind(_this);
}
_this._depth = 0;
_this._cbDepth = 0;
_this._cbs = [];
_this._inCb = null;
_this._cbBuffer = [];
_this._buffer = [];
return _this;
}
/**
* render
* We delay the actual execution of the template function by a tick
* to give us time to bind to the `data`, `error` and `end` events.
*
* @name render
* @function
* @param {Object} locals The template data.
* @param {Function} cb The callback function.
*/
_createClass(VM, [{
key: 'render',
value: function render(locals, cb) {
var _this2 = this;
if (cb) {
streamData(this, function (err, data) {
cb(err, data);
});
}
this._locals = locals || {};
process.nextTick(function () {
return _this2._execute();
});
return this;
}
/**
* _execute
* We kick off the VM by calling the compiled template function,
* passing it our own vm context (for writes and callback handling),
* as well as the locals passed in for the current request.
*
* @name _execute
* @function
*/
}, {
key: '_execute',
value: function _execute() {
this._depth++;
this._function.call(this, this._vmContext(), this._runLocals());
}
/**
* _include
* When you call `include` in a template, we use `Loader` to find
* the appropriate template (using a cached copy if available),
* pass it the context you provide, and execute it under this VM.
*
* @name _include
* @function
* @param {String} request The path to the included ajs file.
* @param {Object} locals The template data.
* @param {Object} pLocals The parent template data.
*/
}, {
key: '_include',
value: function _include(request, locals, pLocals) {
if (!request.endsWith(".ajs")) {
request += ".ajs";
}
var filename = path.resolve(pLocals.__dirname, request);
var template = null;
if (filename == this.filename) throw new Error('self include');
try {
template = Template.loadInclude(filename);
} catch (e) {
if (e.code == 'ENOENT' || e.code == 'EBADF') throw new Error("Can't find include: '" + request + "'");else throw e;
}
var includeLocals = util.extend(this._runLocals(), locals || {});
this._depth++;
template.call(this, this._vmContext(), util.extend({
__filename: template.filename,
__dirname: template.dirname
}, includeLocals, true));
}
/**
* _moduleInclude
* Include an ajs template from node_modules
*
* @name _moduleInclude
* @function
* @param {String} request The path to the included ajs file.
* @param {Object} locals The template data.
* @param {Object} pLocals The parent template data.
*/
}, {
key: '_moduleInclude',
value: function _moduleInclude(request, locals, pLocals) {
if (!request.endsWith(".ajs")) {
request += ".ajs";
}
var filename = require.resolve(request);
return this._include(filename, locals, pLocals);
}
/**
* _render
* Renders the template.
*
* @name _render
* @function
* @param {String} str The string to render.
* @param {Object} locals The template data.
* @param {Object} opts The compile options.
*/
}, {
key: '_render',
value: function _render(str, locals, opts) {
opts = opts || {};
var template = void 0;
if (opts.filename) {
var key = JSON.stringify(opts.filename);
if (!(template = Cache._store[key])) template = Cache._store[key] = new Template(str, opts);
} else template = new Compiler(str, opts).compile();
var renderLocals = util.extend(this._runLocals(), locals || {});
this._depth++;
template.call(this, this._vmContext(), renderLocals);
}
/**
* _wrapCb
* This is where the magic happens. The compiler wraps any arguments
* that look like callbacks with this function, enabling us to keep
* track of when a callback returns and when its completed.
*
* @name _wrapCb
* @function
* @param {Function} func The callback function.
* @returns {Function} The wrapping function.
*/
}, {
key: '_wrapCb',
value: function _wrapCb(func) {
if (typeof func != 'function') return func;
if (this._inCb != null) throw new Error('nested callback');
var id = this._cbBuffer.length,
cb = { data: [], done: false };
this._cbBuffer[id] = cb;
this._cbDepth++;
this._flush();
var self = this;
return function () {
self._cbStart(id);
func.apply(this, arguments);
self._cbEnd(id);
};
}
/**
* _cbStart
* Mark the callback state as *started*.
*
* @name _cbStart
* @function
* @param {String} id The callback id.
*/
}, {
key: '_cbStart',
value: function _cbStart(id) {
this._cbDepth--;
this._cbBuffer[id].done = false;
this._inCb = id;
}
/**
* _cbEnd
* Mark the callback state as *done*.
*
* @name _cbEnd
* @function
* @param {String} id The callback id.
*/
}, {
key: '_cbEnd',
value: function _cbEnd(id) {
this._cbBuffer[id].done = true;
this._inCb = null;
this._write();
}
/**
* _write
* Write data in the buffers.
*
* @name _write
* @function
* @param {String} data The data to write.
*/
}, {
key: '_write',
value: function _write(data) {
var include = void 0;
if (data) {
// We're not waiting on any callbacks, so write directly to the main buffer.
if (this._cbDepth == 0) return this._buffer.push(data);
// If we're currently writing _inside_ a callback, we make sure to write
// to its own buffer. Otherwise we write to the cb buffer so we stay in order.
if (this._inCb != null) this._cbBuffer[this._inCb].data.push(data);else this._cbBuffer.push(data);
}
// Each time we write, check to see if any callbacks have been completed.
// If so, we can dump its buffer into the main buffer and continue until
// we hit the next incomplete callback.
for (var i in this._cbBuffer) {
var cb = null;
if (typeof (cb = this._cbBuffer[i]).done != 'undefined') {
if (cb.done) {
if (cb.data.length) this._buffer.push(cb.data.join(''));
} else return;
} else {
this._buffer.push(this._cbBuffer[i]);
}
delete this._cbBuffer[i];
}
// We don't want to overload the socket with too many writes, so we only
// flush on two occasions: (1) when we start waiting on a callback, and
// (2) when the template has finished rendering.
if (this.isComplete()) {
this._flush();
}
}
/**
* _flush
* Ends the buffer writing and emits the *end* event.
*
* @name _flush
* @function
*/
}, {
key: '_flush',
value: function _flush() {
var _this3 = this;
// If there's anything in the buffer, emit a `data` event
// with the contents of the buffer as a `String`.
if (this._buffer.length) {
this.emit('data', this._buffer.join(''));
this._buffer = [];
}
// If we're done executing, emit an `end` event.
if (this.isComplete()) {
setTimeout(function () {
_this3.emit('end');
}, 10);
}
}
/**
* _line
* Our compiled AJS is instrumented with calls so we can keep track of
* corresponding line numbers in the original AJS source.
*
* @name _line
* @function
* @param {String} i The line number.
*/
}, {
key: '_line',
value: function _line(i) {
this.line = i;
}
/**
* _error
* When an error occurs during template execution, we handle it here so
* we can add additional information and emit an `error` event.
*
* @name _error
* @function
* @param {Error} e The error object.
*/
}, {
key: '_error',
value: function _error(e, filename) {
e.filename = filename;
e.line = this.line;
e.message = e.message + ' at (' + e.filename + ":" + e.line + ')';
this.error = e;
this.emit('error', this.error);
this._flush();
}
/**
* _end
* All templates call `_end()` when they're finished. Since some templates
* are actually nested `include`s, we use `_depth` to let us know when
* we're actually done.
*
* @name _end
* @function
*/
}, {
key: '_end',
value: function _end() {
this._depth--;
this._write();
}
/**
* isComplete
* We know we're done when the parent template is complete and all callbacks have returned.
*
* @name isComplete
* @function
* @returns {Boolean} `true` if the rendering is complete, `false` otherwise.
*/
}, {
key: 'isComplete',
value: function isComplete() {
return this.error || this._depth == 0 && this._cbDepth == 0;
}
/**
* _vmContext
* Here we build the actual VM context that's passed to the compiled AJS code.
* The calls to these functions are added automatically by the compiler.
*
* @name _vmContext
* @function
* @returns {Object} The vm context.
*/
}, {
key: '_vmContext',
value: function _vmContext() {
if (this._vmContext_) return this._vmContext_;
this._vmContext_ = {
cb: this._wrapCb.bind(this),
out: this._write.bind(this),
ln: this._line.bind(this),
end: this._end.bind(this),
err: this._error.bind(this),
inc: this._include.bind(this),
modInclude: this._moduleInclude.bind(this),
ren: this._render.bind(this),
flush: this._flush.bind(this),
esc: util.escape
};
return this._vmContext_;
}
/**
* _runLocals
* Here we extend the context passed into the template by you,
* adding a few helpful global properties along the way.
*
* @name _runLocals
* @function
* @returns {Object} The locals object.
*/
}, {
key: '_runLocals',
value: function _runLocals() {
if (this._runLocals_) return this._runLocals_;
this._runLocals_ = util.extend({}, this._locals);
this._runLocals_.__filename = this.filename;
this._runLocals_.__dirname = this.dirname;
return this._runLocals_;
}
}]);
return VM;
}(EventEmitter);
VM.ROOT = process.cwd();
Template.VM = VM;
module.exports = Template;