can.view.mustache.js | |
---|---|
/*!
* CanJS - 1.1.4 (2013-02-05)
* http://canjs.us/
* Copyright (c) 2013 Bitovi
* Licensed MIT
*/
(function (can, window, undefined) { | |
can/view/mustache/mustache.js | |
mustache.js
InitializationDefine the view extension. | can.view.ext = ".mustache"; |
Setup internal helper variables and functions.An alias for the context variable used for tracking a stack of contexts. This is also used for passing to helper functions to maintain proper context. | var CONTEXT = '___c0nt3xt', |
An alias for the variable used for the hash object that can be passed
to helpers via | HASH = '___h4sh', |
An alias for the function that adds a new context to the context stack. | STACK = '___st4ck', |
An alias for the most used context stacking call. | CONTEXT_STACK = STACK + '(' + CONTEXT + ',this)',
CONTEXT_OBJ = '{context:' + CONTEXT_STACK + ',options:options}',
isObserve = function (obj) {
return obj !== null && can.isFunction(obj.attr) && obj.constructor && !! obj.constructor.canMakeObserve;
},
isArrayLike = function (obj) {
return obj && obj.splice && typeof obj.length == 'number';
}, |
Mustache | Mustache = function (options, helpers) { |
Support calling Mustache without the constructor. This returns a function that renders the template. | if (this.constructor != Mustache) {
var mustache = new Mustache(options);
return function (data, options) {
return mustache.render(data, options);
};
} |
If we get a | if (typeof options == "function") {
this.template = {
fn: options
};
return;
} |
Set options on self. | can.extend(this, options);
this.template = this.scanner.scan(this.text, this.name);
}; |
Put Mustache on the | can.Mustache = window.Mustache = Mustache;
Mustache.prototype.
render = function (object, options) {
object = object || {};
options = options || {};
if (!options.helpers && !options.partials) {
options.helpers = options;
}
return this.template.fn.call(object, object, {
_data: object,
options: options
});
};
can.extend(Mustache.prototype, { |
Share a singleton scanner for parsing templates. | scanner: new can.view.Scanner({ |
A hash of strings for the scanner to inject at certain points. | text: { |
This is the logic to inject at the beginning of a rendered template.
This includes initializing the | start: 'var ' + CONTEXT + ' = this && this.' + STACK + ' ? this : []; ' + CONTEXT + '.' + STACK + ' = true;' + 'var ' + STACK + ' = function(context, self) {' + 'var s;' + 'if (arguments.length == 1 && context) {' + 's = !context.' + STACK + ' ? [context] : context;' + |
Handle helpers with custom contexts (#228) | '} else if (!context.' + STACK + ') {' + 's = [self, context];' + '} else {' + 's = context && context.' + STACK + ' ? context.concat([self]) : ' + STACK + '(context).concat([self]);' + '}' + 'return (s.' + STACK + ' = true) && s;' + '};'
}, |
An ordered token registry for the scanner.
This needs to be ordered by priority to prevent token parsing errors.
Each token follows the following structure:
[
// Which key in the token map to match.
"tokenMapName",
// A simple token to match, like "{{".
"token",
// Optional. A complex (regexp) token to match that
// overrides the simple token.
"[\s\t]*{{",
// Optional. A function that executes advanced
// manipulation of the matched content. This is
// rarely used.
function(content){ | tokens: [ |
Return unescaped | ["returnLeft", "{{{", "{{[{&]"], |
Full line comments | ["commentFull", "{{!}}", "^[\\s\\t]*{{!.+?}}\\n"], |
Inline comments | ["commentLeft", "{{!", "(\\n[\\s\\t]*{{!|{{!)"], |
Full line escapes This is used for detecting lines with only whitespace and an escaped tag | ["escapeFull", "{{}}", "(^[\\s\\t]*{{[#/^][^}]+?}}\\n|\\n[\\s\\t]*{{[#/^][^}]+?}}\\n|\\n[\\s\\t]*{{[#/^][^}]+?}}$)", function (content) {
return {
before: /^\n.+?\n$/.test(content) ? '\n' : '',
content: content.match(/\{\{(.+?)\}\}/)[1] || ''
};
}], |
Return escaped | ["escapeLeft", "{{"], |
Close return unescaped | ["returnRight", "}}}"], |
Close tag | ["right", "}}"]], |
Scanning HelpersThis is an array of helpers that transform content that is within escaped tags like | helpers: [ |
PartialsPartials begin with a greater than sign, like {{> box}}. Partials are rendered at runtime (as opposed to compile time), so recursive partials are possible. Just avoid infinite loops. For example, this template and partial: base.mustache: Names{{#names}} {{> user}} {{/names}} user.mustache: {{name}} | {
name: /^>[\s]*\w*/,
fn: function (content, cmd) { |
Get the template name and call back into the render method, passing the name and the current context. | var templateName = can.trim(content.replace(/^>\s?/, '')).replace(/["|']/g, "");
return "options.partials && options.partials['" + templateName + "'] ? can.Mustache.renderPartial(options.partials['" + templateName + "']," + CONTEXT_STACK + ".pop(),options) : can.Mustache.render('" + templateName + "', " + CONTEXT_STACK + ")";
}
}, |
Data HookupThis will attach the data property of | {
name: /^\s*data\s/,
fn: function (content, cmd) {
var attr = content.match(/["|'](.*)["|']/)[1]; |
return a function which calls | return "can.proxy(function(__){can.data(can.$(__),'" + attr + "', this.pop()); }, " + CONTEXT_STACK + ")";
}
}, |
Transformation (default)This transforms all content to its interpolated equivalent, including calls to the corresponding helpers as applicable. This outputs the render code for almost all cases. Definitions
DesignThis covers the design of the render code that the transformation helper generates. PseudocodeA detailed explanation is provided in the following sections, but here is some brief pseudocode
that gives a high level overview of what the generated render code does (with a template similar to InitializationEach rendered template is started with the following initialization code:
var v1ew = [];
var _c0nt3xt = [];
c0nt3xt.st4ck = true;
var st4ck = function(context, self) {
var s;
if (arguments.length == 1 && context) {
s = !context.st4ck ? [context] : context;
} else {
s = context && context.st4ck
? context.concat([self])
: st4ck(context).concat([self]);
}
return (s.__st4ck = true) && s;
};
The SectionsEach section, Implementation | {
name: /^.*$/,
fn: function (content, cmd) {
var mode = false,
result = []; |
Trim the content so we don't have any trailing whitespace. | content = can.trim(content); |
Determine what the active mode is.
* | if (content.length && (mode = content.match(/^([#^/]|else$)/))) {
mode = mode[0];
switch (mode) { |
Open a new section. | case '#':
case '^':
result.push(cmd.insert + 'can.view.txt(0,\'' + cmd.tagName + '\',' + cmd.status + ',this,function(){ return ');
break; |
Close the prior section. | case '/':
return {
raw: 'return ___v1ew.join("");}}])}));'
};
break;
} |
Trim the mode off of the content. | content = content.substring(1);
} |
| if (mode != 'else') {
var args = [],
i = 0,
hashing = false,
arg, split, m; |
Parse the helper arguments. This needs uses this method instead of a split(/\s/) so that strings with spaces can be correctly parsed. | (can.trim(content) + ' ').replace(/((([^\s]+?=)?('.*?'|".*?"))|.*?)\s/g, function (whole, part) {
args.push(part);
}); |
Start the content render block. | result.push('can.Mustache.txt(' + CONTEXT_OBJ + ',' + (mode ? '"' + mode + '"' : 'null') + ','); |
Iterate through the helper arguments, if there are any. | for (; arg = args[i]; i++) {
i && result.push(','); |
Check for special helper arguments (string/number/boolean/hashes). | if (i && (m = arg.match(/^(('.*?'|".*?"|[0-9.]+|true|false)|((.+?)=(('.*?'|".*?"|[0-9.]+|true|false)|(.+))))$/))) { |
Found a native type like string/number/boolean. | if (m[2]) {
result.push(m[0]);
} |
Found a hash object. | else { |
Open the hash object. | if (!hashing) {
hashing = true;
result.push('{' + HASH + ':{');
} |
Add the key/value. | result.push(m[4], ':', m[6] ? m[6] : 'can.Mustache.get("' + m[5].replace(/"/g, '\\"') + '",' + CONTEXT_OBJ + ')'); |
Close the hash if this was the last argument. | if (i == args.length - 1) {
result.push('}}');
}
}
} |
Otherwise output a normal interpolation reference. | else {
result.push('can.Mustache.get("' + |
Include the reference name. | arg.replace(/"/g, '\\"') + '",' + |
Then the stack of context. | CONTEXT_OBJ + |
Flag as a helper method to aid performance, if it is a known helper (anything with > 0 arguments). | (i == 0 && args.length > 1 ? ',true' : ',false') + (i > 0 ? ',true' : ',false') + ')');
}
}
} |
Create an option object for sections of code. | mode && mode != 'else' && result.push(',[{_:function(){');
switch (mode) { |
Truthy section | case '#':
result.push('return ___v1ew.join("");}},{fn:function(' + CONTEXT + '){var ___v1ew = [];');
break; |
If/else section Falsey section | case 'else':
case '^':
result.push('return ___v1ew.join("");}},{inverse:function(' + CONTEXT + '){var ___v1ew = [];');
break; |
Not a section | default:
result.push(');');
break;
} |
Return a raw result if there was a section, otherwise return the default string. | result = result.join('');
return mode ? {
raw: result
} : result;
}
}]
})
}); |
Add in default scanner helpers first. We could probably do this differently if we didn't 'break' on every match. | var helpers = can.view.Scanner.prototype.helpers;
for (var i = 0; i < helpers.length; i++) {
Mustache.prototype.scanner.helpers.unshift(helpers[i]);
};
Mustache.txt = function (context, mode, name) { |
Grab the extra arguments to pass to helpers. | var args = Array.prototype.slice.call(arguments, 3), |
Create a default | options = can.extend.apply(can, [{
fn: function () {},
inverse: function () {}
}].concat(mode ? args.pop() : []));
var extra = {};
if (context.context) {
extra = context.options;
context = context.context;
} |
Check for a registered helper or a helper-like function. | if (helper = (Mustache.getHelper(name, extra) || (can.isFunction(name) && !name.isComputed && {
fn: name
}))) { |
Use the most recent context as | var context = (context[STACK] && context[context.length - 1]) || context, |
Update the options with a function/inverse (the inner templates of a section). | opts = {
fn: can.proxy(options.fn, context),
inverse: can.proxy(options.inverse, context)
},
lastArg = args[args.length - 1]; |
Add the hash to | if (lastArg && lastArg[HASH]) {
opts.hash = args.pop()[HASH];
}
args.push(opts); |
Call the helper. | return helper.fn.apply(context, args) || '';
} |
if a compute, get the value | if (can.isFunction(name) && name.isComputed) {
name = name();
} |
An array of arguments to check for truthyness when evaluating sections. | var validArgs = args.length ? args : [name], |
Whether the arguments meet the condition of the section. | valid = true,
result = [],
i, helper, argIsObserve, arg; |
Validate the arguments based on the section mode. | if (mode) {
for (i = 0; i < validArgs.length; i++) {
arg = validArgs[i];
argIsObserve = typeof arg !== 'undefined' && isObserve(arg); |
Array-like objects are falsey if their length = 0. | if (isArrayLike(arg)) { |
Use .attr to trigger binding on empty lists returned from function | if (mode == '#') {
valid = valid && !! (argIsObserve ? arg.attr('length') : arg.length);
} else if (mode == '^') {
valid = valid && !(argIsObserve ? arg.attr('length') : arg.length);
}
} |
Otherwise just check if it is truthy or not. | else {
valid = mode == '#' ? valid && !! arg : mode == '^' ? valid && !arg : valid;
}
}
} |
Otherwise interpolate like normal. | if (valid) {
switch (mode) { |
Truthy section. | case '#': |
Iterate over arrays | if (isArrayLike(name)) {
var isObserveList = isObserve(name); |
Add the reference to the list in the contexts. | for (i = 0; i < name.length; i++) {
result.push(options.fn.call(name[i] || {}, context) || ''); |
Ensure that live update works on observable lists | isObserveList && name.attr('' + i);
}
return result.join('');
} |
Normal case. | else {
return options.fn.call(name || {}, context) || '';
}
break; |
Falsey section. | case '^':
return options.inverse.call(name || {}, context) || '';
break;
default: |
Add + '' to convert things like numbers to strings. This can cause issues if you are trying to eval on the length but this is the more common case. | return '' + (name !== undefined ? name : '');
break;
}
}
return '';
};
Mustache.get = function (ref, contexts, isHelper, isArgument) {
var options = contexts.options || {};
contexts = contexts.context || contexts; |
Split the reference (like | var names = ref.split('.'),
namesLength = names.length, |
Assume the local object is the last context in the stack. | obj = contexts[contexts.length - 1], |
Assume the parent context is the second to last context in the stack. | context = contexts[contexts.length - 2],
lastValue, value, name, i, j, |
if we walk up and don't find a property, we default to listening on an undefined property of the first context that is an observe | defaultObserve, defaultObserveName; |
Handle | if (/^\.|this$/.test(ref)) { |
If context isn't an object, then it was a value passed by a helper so use it as an override. | if (!/^object|undefined$/.test(typeof context)) {
return context || '';
} |
Otherwise just return the closest object. | else {
while (value = contexts.pop()) {
if (typeof value !== 'undefined') {
return value;
}
}
return '';
}
} |
Handle object resolution (like | else if (!isHelper) { |
Reverse iterate through the contexts (last in, first out). | for (i = contexts.length - 1; i >= 0; i--) { |
Check the context for the reference | value = contexts[i]; |
Make sure the context isn't a failed object before diving into it. | if (value !== undefined) {
for (j = 0; j < namesLength; j++) { |
Keep running up the tree while there are matches. | if (typeof value[names[j]] != 'undefined') {
lastValue = value;
value = value[name = names[j]];
} |
If it's undefined, still match if the parent is an Observe. | else if (isObserve(value)) {
defaultObserve = value;
defaultObserveName = names[j];
lastValue = value = undefined;
break;
}
else {
lastValue = value = undefined;
break;
}
}
} |
Found a matched reference. | if (value !== undefined) {
if (can.isFunction(lastValue[name]) && isArgument) { |
Don't execute functions if they are parameters for a helper and are not a can.compute Need to bind it to the original context so that that information doesn't get lost by the helper | return function () {
return lastValue[name].apply(lastValue, arguments);
};
} else if (can.isFunction(lastValue[name])) { |
Support functions stored in objects. | return lastValue[name]();
} |
Invoke the length to ensure that Observe.List events fire. | else if (isObserve(value) && isArrayLike(value) && value.attr('length')) {
return value;
} |
Add support for observes | else if (isObserve(lastValue)) {
return lastValue.compute(name);
}
else {
return value;
}
}
}
}
if (defaultObserve && |
if there's not a helper by this name and no attribute with this name | !(Mustache.getHelper(ref) && can.inArray(defaultObserveName, can.Observe.keys(defaultObserve)) === -1)) {
return defaultObserve.compute(defaultObserveName);
} |
Support helper-like functions as anonymous helpers | if (obj !== undefined && can.isFunction(obj[ref])) {
return obj[ref];
} |
Support helpers without arguments, but only if there wasn't a matching data reference. | else if (value = Mustache.getHelper(ref, options)) {
return ref;
}
return '';
}; |
HelpersHelpers are functions that can be called from within a template.
These helpers differ from the scanner helpers in that they execute
at runtime instead of during compilation.
Custom helpers can be added via | Mustache._helpers = {};
Mustache.registerHelper = function (name, fn) {
this._helpers[name] = {
name: name,
fn: fn
};
};
Mustache.getHelper = function (name, options) {
return options && options.helpers && options.helpers[name] && {
fn: options.helpers[name]
} || this._helpers[name]
for (var i = 0, helper; helper = [i]; i++) { |
Find the correct helper | if (helper.name == name) {
return helper;
}
}
return null;
};
Mustache.render = function (partial, context) { |
Make sure the partial being passed in isn't a variable like { partial: "foo.mustache" } | if (!can.view.cached[partial] && context[partial]) {
partial = context[partial];
} |
Call into | return can.view.render(partial, context);
};
Mustache.renderPartial = function (partial, context, options) {
return partial.render ? partial.render(context, options) : partial(context, options);
}; |
The built-in Mustache helpers. | can.each({ |
Implements the | 'if': function (expr, options) {
if ( !! expr) {
return options.fn(this);
}
else {
return options.inverse(this);
}
}, |
Implements the | 'unless': function (expr, options) {
if (!expr) {
return options.fn(this);
}
}, |
Implements the | 'each': function (expr, options) {
if ( !! expr && expr.length) {
var result = [];
for (var i = 0; i < expr.length; i++) {
result.push(options.fn(expr[i]));
}
return result.join('');
}
}, |
Implements the | 'with': function (expr, options) {
if ( !! expr) {
return options.fn(expr);
}
}
}, function (fn, name) {
Mustache.registerHelper(name, fn);
}); |
RegistrationRegisters Mustache with can.view. | can.view.register({
suffix: "mustache",
contentType: "x-mustache-template", |
Returns a | script: function (id, src) {
return "can.Mustache(function(_CONTEXT,_VIEW) { " + new Mustache({
text: src,
name: id
}).template.out + " })";
},
renderer: function (id, text) {
return Mustache({
text: text,
name: id
});
}
});
})(can, this);
|