can-ejs
Version:
legacy EJS layer for canjs
234 lines (223 loc) • 6.26 kB
JavaScript
// # can/view/ejs/ejs.js
//
// `can.EJS`: Embedded JavaScript Templates
//
var legacyHelpers = require('can-legacy-view-helpers');
var extend = require("can-util/js/assign/assign");
var namespace = require("can-namespace");
var each = require("can-util/js/each/each");
var canReflect = require("can-reflect");
var observationReader = require("can-stache-key");
var DOCUMENT = require('can-globals/document/document');
var view = legacyHelpers.view;
var templateId = 0;
// ## Helper methods
var EJS = function (options) {
// Supports calling EJS without the constructor.
// This returns a function that renders the template.
if (!this || this.constructor !== EJS) {
var ejs = new EJS(options);
var renderer = function (data, helpers) {
return legacyHelpers.view.frag( ejs.render(data, helpers) );
};
renderer.renderType = "fragment";
renderer.renderAsString = function (data, helpers) {
return ejs.render(data, helpers);
};
renderer.renderAsString.renderType = "string";
return renderer;
}
// If we get a `function` directly, it probably is coming from
// a `steal`-packaged view.
if (typeof options === 'function') {
this.template = {
fn: options
};
return;
}
if(typeof options === 'string') {
options = {
text: options,
name: ""+(++templateId)
};
}
// Set options on self.
extend(this, options);
this.template = this.scanner.scan(this.text, this.name);
};
// Expose EJS via the `can` object.
namespace.EJS = EJS;
EJS.prototype.
// ## Render
// Render a view object with data and helpers.
render = function (object, extraHelpers) {
object = object || {};
return this.template.fn.call(object, object, new EJS.Helpers(object, extraHelpers || {}));
};
extend(EJS.prototype, {
// ## Scanner
// Singleton scanner instance for parsing templates. See [scanner.js](scanner.html)
// for more information.
//
// ### Text
//
// #### Definitions
//
// * `outStart` - Wrapper start text for view function.
//
// * `outEnd` - Wrapper end text for view function.
//
// * `argNames` - Arguments passed into view function.
scanner: new legacyHelpers.Scanner({
text: {
outStart: 'with(_VIEW) { with (_CONTEXT) {',
outEnd: "}}",
argNames: '_CONTEXT,_VIEW',
context: "this"
},
// ### Tokens
//
// An ordered token registry for the scanner. Scanner makes evaluations
// based on which tags are considered opening/closing as well as escaped, etc.
tokens: [
["templateLeft", "<%%"],
["templateRight", "%>"],
["returnLeft", "<%=="],
["escapeLeft", "<%="],
["commentLeft", "<%#"],
["left", "<%"],
["right", "%>"],
["returnRight", "%>"]
],
// ### Helpers
helpers: [
{
// Regex to see if its a func like `()->`.
name: /\s*\(([\$\w]+)\)\s*->([^\n]*)/,
// Evaluate rocket syntax function with correct context.
fn: function (content) {
var quickFunc = /\s*\(([\$\w]+)\)\s*->([^\n]*)/,
parts = content.match(quickFunc);
return "(function(__){var " + parts[1] + "=__;" + parts[2] + "}).bind(this);";
}
}
],
// ### transform
// Transforms the EJS template to add support for shared blocks.
// Essentially, this breaks up EJS tags into multiple EJS tags
// if they contained unmatched brackets.
//
// For example, this doesn't work:
//
// `<% if (1) { %><% if (1) { %> hi <% } } %>`
//
// ...without isolated EJS blocks:
//
// `<% if (1) { %><% if (1) { %> hi <% } %><% } %>`
//
// The result of transforming:
//
// `<% if (1) { %><% %><% if (1) { %><% %> hi <% } %><% } %>`
transform: function (source) {
return source.replace(/<%([\s\S]+?)%>/gm, function (whole, part) {
var brackets = [],
foundBracketPair, i;
// Look for brackets (for removing self-contained blocks)
part.replace(/[{}]/gm, function (bracket, offset) {
brackets.push([
bracket,
offset
]);
});
// Remove bracket pairs from the list of replacements
do {
foundBracketPair = false;
for (i = brackets.length - 2; i >= 0; i--) {
if (brackets[i][0] === '{' && brackets[i + 1][0] === '}') {
brackets.splice(i, 2);
foundBracketPair = true;
break;
}
}
} while (foundBracketPair);
// Unmatched brackets found, inject EJS tags
if (brackets.length >= 2) {
var result = ['<%'],
bracket, last = 0;
for (i = 0; bracket = brackets[i]; i++) {
result.push(part.substring(last, last = bracket[1]));
if (bracket[0] === '{' && i < brackets.length - 1 || bracket[0] === '}' && i > 0) {
result.push(bracket[0] === '{' ? '{ %><% ' : ' %><% }');
} else {
result.push(bracket[0]);
}
++last;
}
result.push(part.substring(last), '%>');
return result.join('');
}
// Otherwise return the original
else {
return '<%' + part + '%>';
}
});
}
})
});
// ## Helpers
//
// In your EJS view you can then call the helper on an element tag:
//
// `<div <%= upperHtml('javascriptmvc') %>></div>`
EJS.Helpers = function (data, extras) {
this._data = data;
this._extras = extras;
this.can = namespace;
extend(this, extras);
};
EJS.Helpers.prototype = {
// List allows for live binding a can.List easily within a template.
list: function (list, cb) {
if(canReflect.isObservableLike(list) && canReflect.isListLike(list)) {
observationReader.get(list, 'length');
}
each(list, function (item, i) {
cb(item, i, list);
});
},
// `each` iterates through a enumerated source(such as can.List or array)
// and sets up live binding when possible.
each: function (list, cb) {
// Normal arrays don't get live updated
if (Array.isArray(list)) {
this.list(list, cb);
} else {
legacyHelpers.view.lists(list, cb);
}
}
};
var templates = {};
EJS.from = function(id){
if(!templates[id]) {
var el = DOCUMENT().getElementById(id);
templates[id] = EJS(el.innerHTML);
}
return templates[id];
};
view.register({
suffix: 'ejs',
script: function (id, src) {
return 'can.EJS(function(_CONTEXT,_VIEW) { ' + new EJS({
text: src,
name: id
})
.template.out + ' })';
},
renderer: function (id, text) {
return EJS({
text: text,
name: id
});
}
});
module.exports = EJS;