/* * Copyright 2011 eBay Software Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * This module provides the lightweight runtime for loading and rendering * templates. The compilation is handled by code that is part of the * [marko/compiler](https://github.com/raptorjs/marko/tree/master/compiler) * module. If rendering a template on the client, only the runtime is needed * on the client and not the compiler */ // async-writer provides all of the magic to support asynchronous // rendering to a stream 'use strict'; /** * Method is for internal usage only. This method * is invoked by code in a compiled Marko template and * it is used to create a new Template instance. * @private */ exports.c = function createTemplate(path) { return new Template(path); }; var BUFFER_OPTIONS = { buffer: true }; var asyncWriter = require('async-writer'); // helpers provide a core set of various utility methods // that are available in every template (empty, notEmpty, etc.) var helpers = require('./helpers'); var loader; // If the optional "stream" module is available // then Readable will be a readable stream var Readable; var AsyncWriter = asyncWriter.AsyncWriter; var extend = require('raptor-util/extend'); exports.AsyncWriter = AsyncWriter; var stream; var STREAM = 'stream'; var streamPath; try { streamPath = require.resolve(STREAM); } catch(e) {} if (streamPath) { stream = require(streamPath); } function renderCallback(renderFunc, data, globalData, callback) { var out = new AsyncWriter(); if (globalData) { extend(out.global, globalData); } renderFunc(data, out); return out.end() .on('finish', function() { callback(null, out.getOutput(), out); }) .once('error', callback); } function Template(path, func, options) { this.path = path; this._ = func; this._options = !options || options.buffer !== false ? BUFFER_OPTIONS : null; } Template.prototype = { /** * Internal method to initialize a loaded template with a * given create function that was generated by the compiler. * Warning: User code should not depend on this method. * * @private * @param {Function(__helpers)} createFunc The function used to produce the render(data, out) function. */ c: function(createFunc) { this._ = createFunc(helpers); }, renderSync: function(data) { var out = new AsyncWriter(); out.sync(); if (data.$global) { out.global = extend(out.global, data.$global); delete data.$global; } this._(data, out); return out.getOutput(); }, /** * Renders a template to either a stream (if the last * argument is a Stream instance) or * provides the output to a callback function (if the last * argument is a Function). * * Supported signatures: * * render(data, callback) * render(data, out) * render(data, stream) * render(data, out, callback) * render(data, stream, callback) * * @param {Object} data The view model data for the template * @param {AsyncWriter} out A Stream or an AsyncWriter instance * @param {Function} callback A callback function * @return {AsyncWriter} Returns the AsyncWriter instance that the template is rendered to */ render: function(data, out, callback) { var renderFunc = this._; var finalData; var globalData; if (data) { finalData = data; if ((globalData = data.$global)) { // We will *move* the "$global" property // into the "out.global" object delete data.$global; } } else { finalData = {}; } if (typeof out === 'function') { // Short circuit for render(data, callback) return renderCallback(renderFunc, finalData, globalData, out); } // NOTE: We create new vars here to avoid a V8 de-optimization due // to the following: // Assignment to parameter in arguments object var finalOut = out; var shouldEnd = false; if (arguments.length === 3) { // render(data, out, callback) if (!finalOut || !finalOut.isAsyncWriter) { finalOut = new AsyncWriter(finalOut); shouldEnd = true; } finalOut .on('finish', function() { callback(null, finalOut.getOutput(), finalOut); }) .once('error', callback); } else if (!finalOut || !finalOut.isAsyncWriter) { // Assume the "finalOut" is really a stream // // By default, we will buffer rendering to a stream to prevent // the response from being "too chunky". finalOut = asyncWriter.create(finalOut, this._options); shouldEnd = true; } if (globalData) { extend(finalOut.global, globalData); } // Invoke the compiled template's render function to have it // write out strings to the provided out. renderFunc(finalData, finalOut); // Automatically end output stream (the writer) if we // had to create an async writer (which might happen // if the caller did not provide a writer/out or the // writer/out was not an AsyncWriter). // // If out parameter was originally an AsyncWriter then // we assume that we are writing to output that was // created in the context of another rendering job. return shouldEnd ? finalOut.end() : finalOut; }, stream: function(data) { if (!stream) { throw new Error('Module not found: stream'); } return new Readable(this, data, this._options); } }; if (stream) { Readable = function(template, data, options) { Readable.$super.call(this); this._t = template; this._d = data; this._options = options; this._rendered = false; }; Readable.prototype = { write: function(data) { if (data != null) { this.push(data); } }, end: function() { this.push(null); }, _read: function() { if (this._rendered) { return; } this._rendered = true; var template = this._t; var data = this._d; var out = asyncWriter.create(this, this._options); template.render(data, out); out.end(); } }; require('raptor-util/inherit')(Readable, stream.Readable); } function createRenderProxy(template) { return function(data, out) { template._(data, out); }; } function initTemplate(rawTemplate, templatePath) { if (rawTemplate.render) { return rawTemplate; } var createFunc = rawTemplate.create || rawTemplate; var template = createFunc.loaded; if (!template) { template = createFunc.loaded = new Template(templatePath); template.c(createFunc); } return template; } function load(templatePath, templateSrc, options) { if (!templatePath) { throw new Error('"templatePath" is required'); } if (arguments.length === 1) { // templateSrc and options not provided } else if (arguments.length === 2) { // see if second argument is templateSrc (a String) // or options (an Object) var lastArg = arguments[arguments.length - 1]; if (typeof lastArg !== 'string') { options = arguments[1]; templateSrc = undefined; } } else if (arguments.length === 3) { // assume function called according to function signature } else { throw new Error('Illegal arguments'); } var template; if (typeof templatePath === 'string') { template = initTemplate(loader(templatePath, templateSrc, options), templatePath); } else if (templatePath.render) { template = templatePath; } else { template = initTemplate(templatePath); } if (options && (options.buffer != null)) { template = new Template( template.path, createRenderProxy(template), options); } return template; } exports.load = load; exports.createWriter = function(writer) { return new AsyncWriter(writer); }; exports.helpers = helpers; exports.Template = Template; // The loader is used to load templates that have not already been // loaded and cached. On the server, the loader will use // the compiler to compile the template and then load the generated // module file using the Node.js module loader loader = require('./loader');