mirror of
https://github.com/marko-js/marko.git
synced 2025-12-08 19:26:05 +00:00
327 lines
9.2 KiB
JavaScript
327 lines
9.2 KiB
JavaScript
/*
|
|
* 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');
|