UNPKG

ajs

Version:

Asynchronous templating in Node.js

291 lines (255 loc) 8.81 kB
"use strict"; var fs = require("fs"), path = require("path"), Template = require("./template"), Cache = require("./cache"), idy = require("idy"); // If you need lower-level access to an ajs template, simply require it, call it // with a locals object `template(<locals>)`, and bind to its `data`, // `error` and `end` events. require.extensions[".ajs"] = function (module, filename) { module.exports = ajs._loadSync(filename); return module; }; /** * ajs * The main `ajs` export is a Connect middleware function. By adding * `ajs()` to your stack, any middleware down the line will have a * `res.render("/path", <locals>)` function that accepts a template * path and context object. * * @name ajs * @function * @param {Object} opts An object containing the following fields: * * - `dir` (String): The path to the views directory (default: `./views`). * * @returns {Function} The middleware function. */ function ajs(opts) { opts = opts || {}; var templateDir = opts.dir || "./views"; return function (req, res, next) { res.render = function (filename, locals, opts) { filename = normalizeFilename(path.join(templateDir, filename)); ajs._load(filename, opts, function (err, template) { if (err) { if (err.code == "ENOENT" || err.code == "EBADF") { res.statusCode = 500; res.end("Template not found: " + filename); } else { next(err); } return; } // We make sure to set the content-type and transfer-encoding headers // to take full advantage of HTTP's streaming ability. res.statusCode = 200; res.setHeader("Content-Type", "text/html; charset=UTF-8"); res.setHeader("Transfer-Encoding", "chunked"); // As data becomes available from the template, we pass it on to the client immediately. template(locals).on("data", function (data) { res.write(data); }).on("error", function (e) { console.error(e.stack); res.statusCode = 500; res.end("Internal server error"); }).on("end", function () { res.end(); }); next(); }); }; }; } /** * serve * If you're looking for a simpler way to build a quick templated site, * you can use the `ajs.serve("dir", <locals>)` middleware and ajs will map request URLs * directly to file and directory paths. Simply create a context containing * a data source and any utilities, and your entire app can live in your templates! * If this reminds of you PHP, just remember you're asyncronous now. * * @name serve * @function * @param {String} rootDir The views directory. * @param {Object} locals The data to pass. * @param {Object} opts The render options. * @returns {Function} The middleware function. */ ajs.serve = function (rootDir, locals, opts) { return function (req, res, next) { var path = normalizeFilename(req.url), filename = rootDir + path; ajs._load(filename, opts, function (err, template) { if (err) { return next(err); } locals.request = req; template(locals).on("data", function (data) { res.write(data); }).on("error", function (e) { console.error(e.stack); res.statusCode = 500; res.end("Internal server error"); }).on("end", function () { res.end(); }); }); }; }; /** * compile * While we can't support ExpressJS yet due to its syncronous handling of * [template engines](https://github.com/visionmedia/express/blob/master/lib/view.js#L421) * and [responses](https://github.com/visionmedia/express/blob/master/lib/response.js#L115), * we can still support a similar API. * * @name compile * @function * @param {String} str The content to compile. * @param {Object} opts An object containing the following fields: * * - `filename` (String): The filename of the compiled file. By default a random filename. * - * @returns {Template} An ajs `Template` object. */ ajs.compile = function (str, opts) { opts = opts || {}; opts.filename = opts.filename || idy(); var key = JSON.stringify(opts.filename + opts.bare), template = void 0; if (!(template = Cache._store[key])) template = Cache._store[key] = new Template(str, opts); return template; }; /** * render * Render the template content. * * @name render * @function * @param {String} str The template content. * @param {Object} opts The compile options. * @param {Function} callback The callback function. * @returns {EventEmitter} The event emitter you can use to listen to `'data'`, * `'end'`, and `'error'` events. */ ajs.render = function (str, opts, callback) { var buffer = [], template = ajs.compile(str, opts), ev = template(opts.locals); if (callback) { ev.on("data", function (data) { buffer.push(data); }).on("error", function (err) { callback(err); }).on("end", function () { callback(null, buffer.join("")); }); } return ev; }; /** * renderFile * Renders a file. * * @name renderFile * @function * @param {String} path The path to the template file. * @param {Object} opts The compile options. * @param {Function} callback The callback function. */ ajs.renderFile = function (path, data, opts, callback) { if (typeof data === "function") { callback = data; opts = {}; } else if (typeof opts === "function") { callback = opts; opts = {}; } opts = opts || {}; opts.locals = data; ajs.compileFile(path, opts, function (err, template) { if (err) { return callback(err); } template(opts.locals, callback); }); }; // Alias for express rendering ajs.__express = ajs.renderFile; /** * compileFile * Return a template function compiled from the requested file. * If a cached object is found and the file hasn't been updated, return that. * Otherwise, attempt to read and compile the file asyncronously, calling back * with a compiled template function if successful or an error if not. * * @name compileFile * @function * @param {String} filename The path to the file. * @param {Object} opts The compile options. * @param {Function} callback The callback function. */ ajs.compileFile = ajs._load = function (filename, opts, callback) { if (typeof opts === "function") { callback = opts; opts = {}; } opts = opts || {}; var template = void 0, cache = typeof opts.cache != "undefined" ? opts.cache : true; Cache.get(filename, function (err, cached) { if (err) return callback(err); if (cache && cached) { callback(null, cached); } else { fs.readFile(filename, "utf-8", function (err, source) { if (err) return callback(err); try { opts.filename = filename; template = new Template(source, opts); } catch (e) { e.message = "In " + filename + ", " + e.message; return callback(e); } Cache.set(filename, template); callback(null, template); }); } }); }; /** * compileFileSync * Synchronous version of `ajs.compileFile`, used for `require()` support. * * @name compileFileSync * @function * @param {String} filename The path to the file. * @param {Object} opts The compile options. * @returns {Template} The ajs template object. */ ajs.compileFileSync = ajs._loadSync = function (filename, opts) { opts = opts || {}; var template = void 0, cache = typeof opts.cache != "undefined" ? opts.cache : true; try { if (cache && (cached = Cache.getSync(filename))) { return cached; } else { opts.filename = filename; template = new Template(fs.readFileSync(filename, "utf8"), opts); Cache.set(filename, template); return template; } } catch (e) { e.message = "In " + filename + ", " + e.message; throw e; } }; function normalizeFilename(path) { if (path.slice(-1) == "/") path += "index"; if (path.slice(-4) != ".ajs") path += ".ajs"; return path; } module.exports = ajs;