can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
450 lines (399 loc) • 16 kB
JavaScript
// # can/view/stache/mustache_core.js
//
// This provides helper utilities for Mustache processing. Currently,
// only stache uses these helpers. Ideally, these utilities could be used
// in other libraries implementing Mustache-like features.
steal("can/util",
"./utils",
"./mustache_helpers",
"./expression.js",
"can/view/live",
"can/view/elements.js",
"can/view/scope",
"can/view/node_lists",
function(can, utils, mustacheHelpers, expression, live, elements, Scope, nodeLists ){
live = live || can.view.live;
elements = elements || can.view.elements;
Scope = Scope || can.view.Scope;
nodeLists = nodeLists || can.view.nodeLists;
// ## Types
// A lookup is an object that is used to identify a lookup in the scope.
/**
* @hide
* @typedef {{get: String}} can.mustache.Lookup
* @option {String} get A value in the scope to look up.
*/
// ## Helpers
var mustacheLineBreakRegExp = /(?:(?:^|(\r?)\n)(\s*)(\{\{([^\}]*)\}\}\}?)([^\S\n\r]*)($|\r?\n))|(\{\{([^\}]*)\}\}\}?)/g,
// A helper for calling the truthy subsection for each item in a list and returning them in a string.
getItemsStringContent = function(items, isObserveList, helperOptions, options){
var txt = "";
for (var i = 0, len = items.length; i < len; i++) {
txt += helperOptions.fn( isObserveList ? items.attr('' + i) : items[i], options);
}
return txt;
},
k = function(){};
var core = {
expression: expression,
// ## mustacheCore.makeEvaluator
// Given a scope and expression, returns a function that evaluates that expression in the scope.
//
// This function first reads lookup values in the args and hash. Then it tries to figure out
// if a helper is being called or a value is being read. Finally, depending on
// if it's a helper, or not, and which mode the expression is in, it returns
// a function that can quickly evaluate the expression.
/**
* @hide
* Given a mode and expresion data, returns a function that evaluates that expression.
* @param {can.view.Scope} The scope in which the expression is evaluated.
* @param {can.view.Options} The option helpers in which the expression is evaluated.
* @param {String} mode Either null, #, ^. > is handled elsewhere
* @param {Object} exprData Data about what was in the mustache expression
* @param {renderer} [truthyRenderer] Used to render a subsection
* @param {renderer} [falseyRenderer] Used to render the inverse subsection
* @param {String} [stringOnly] A flag to indicate that only strings will be returned by subsections.
* @return {Function} An 'evaluator' function that evaluates the expression.
*/
makeEvaluator: function (scope, helperOptions, nodeList, mode, exprData, truthyRenderer, falseyRenderer, stringOnly) {
if(mode === "^") {
var temp = truthyRenderer;
truthyRenderer = falseyRenderer;
falseyRenderer = temp;
}
var value,
helperOptionArg;
if(exprData instanceof expression.Call) {
helperOptionArg = {
fn: function () {},
inverse: function () {},
context: scope.attr("."),
scope: scope,
nodeList: nodeList,
exprData: exprData,
helpersScope: helperOptions
};
utils.convertToScopes(helperOptionArg, scope,helperOptions, nodeList, truthyRenderer, falseyRenderer, stringOnly);
value = exprData.value(scope, helperOptions, helperOptionArg);
if(exprData.isHelper) {
return value;
}
} else {
var readOptions = {
// will return a function instead of calling it.
// allowing it to be turned into a compute if necessary.
isArgument: true,
args: [scope.attr('.'), scope],
asCompute: true
};
var helperAndValue = exprData.helperAndValue(scope, helperOptions, readOptions, nodeList, truthyRenderer, falseyRenderer, stringOnly);
var helper = helperAndValue.helper;
value = helperAndValue.value;
if(helper) {
return exprData.evaluator(helper, scope, helperOptions, readOptions, nodeList, truthyRenderer, falseyRenderer, stringOnly);
}
}
// Return evaluators for no mode.
if(!mode) {
// If it's computed, return a function that just reads the compute.
if(value && value.isComputed) {
return value;
}
// Just return value as the value
else {
return function(){
return '' + (value != null ? value : '');
};
}
} else if( mode === "#" || mode === "^" ) {
// Setup renderers.
helperOptionArg = {
fn: function () {},
inverse: function () {}
};
utils.convertToScopes(helperOptionArg, scope, helperOptions, nodeList, truthyRenderer, falseyRenderer, stringOnly);
return function(){
// Get the value
var finalValue;
if (can.isFunction(value) && value.isComputed) {
finalValue = value();
} else {
finalValue = value;
}
if(typeof finalValue === "function") {
return finalValue;
}
// If it's an array, render.
else if (utils.isArrayLike(finalValue) ) {
var isObserveList = utils.isObserveLike(finalValue);
if(isObserveList ? finalValue.attr("length") : finalValue.length) {
if (stringOnly) {
return getItemsStringContent(finalValue, isObserveList, helperOptionArg, helperOptions);
} else {
return can.frag(utils.getItemsFragContent(finalValue, helperOptionArg, scope));
}
} else {
return helperOptionArg.inverse(scope, helperOptions);
}
}
// If truthy, render fn, otherwise, inverse.
else {
return finalValue ? helperOptionArg.fn(finalValue || scope, helperOptions) : helperOptionArg.inverse(scope, helperOptions);
}
};
} else {
// not supported!
}
},
// ## mustacheCore.makeLiveBindingPartialRenderer
// Returns a renderer function that live binds a partial.
/**
* @hide
* Returns a renderer function that live binds a partial.
* @param {String} partialName the name of the partial.
* @return {function(this:HTMLElement,can.view.Scope,can.view.Options)} A renderer function
* live binds a partial.
*/
makeLiveBindingPartialRenderer: function(partialName, state){
partialName = can.trim(partialName);
return function(scope, options, parentSectionNodeList){
var nodeList = [this];
nodeList.expression = ">" + partialName;
nodeLists.register(nodeList, null, parentSectionNodeList || true, state.directlyNested);
var partialFrag = can.compute(function(){
var localPartialName = partialName;
// Look up partials in options first.
var partial = options.attr("partials." + localPartialName), renderer;
if (partial) {
renderer = function() {
return partial.render ? partial.render(scope, options, nodeList)
: partial(scope, options);
};
}
// Use can.view to get and render the partial.
else {
var scopePartialName = scope.read(localPartialName, {
isArgument: true
}).value;
if (scopePartialName === null || !scopePartialName && localPartialName[0] === '*') {
return can.frag("");
}
if (scopePartialName) {
localPartialName = scopePartialName;
}
renderer = function() {
return can.isFunction(localPartialName) ? localPartialName(scope, options, nodeList)
: can.view.render(localPartialName, scope, options, nodeList);
};
}
var res = can.__notObserve(renderer)();
return can.frag(res);
});
partialFrag.computeInstance.setPrimaryDepth(nodeList.nesting);
live.html(this, partialFrag, this.parentNode, nodeList);
};
},
// ## mustacheCore.makeStringBranchRenderer
// Return a renderer function that evalutes to a string and caches
// the evaluator on the scope.
/**
* @hide
* Return a renderer function that evaluates to a string.
* @param {String} mode
* @param {can.mustache.Expression} expression
* @return {function(can.view.Scope,can.view.Options, can.view.renderer, can.view.renderer)}
*/
makeStringBranchRenderer: function(mode, expressionString){
var exprData = core.expression.parse(expressionString),
// Use the full mustache expression as the cache key.
fullExpression = mode+expressionString;
// convert a lookup like `{{value}}` to still be called as a helper if necessary.
if(!(exprData instanceof expression.Helper) && !(exprData instanceof expression.Call)) {
exprData = new expression.Helper(exprData,[],{});
}
// A branching renderer takes truthy and falsey renderer.
return function branchRenderer(scope, options, truthyRenderer, falseyRenderer){
// Check the scope's cache if the evaluator already exists for performance.
var evaluator = scope.__cache[fullExpression];
if(mode || !evaluator) {
evaluator = makeEvaluator( scope, options, null, mode, exprData, truthyRenderer, falseyRenderer, true);
if(!mode) {
scope.__cache[fullExpression] = evaluator;
}
}
// Run the evaluator and return the result.
var res = evaluator();
return res == null ? "" : ""+res;
};
},
// ## mustacheCore.makeLiveBindingBranchRenderer
// Return a renderer function that evaluates the mustache expression and
// sets up live binding if a compute with dependencies is found. Otherwise,
// the element's value is set.
//
// This function works by creating a `can.compute` from the mustache expression.
// If the compute has dependent observables, it passes the compute to `can.view.live`; otherwise,
// it updates the element's property based on the compute's value.
/**
* @hide
* Returns a renderer function that evaluates the mustache expression.
* @param {String} mode
* @param {can.mustache.Expression} expression
* @param {Object} state The html state of where the expression was found.
*/
makeLiveBindingBranchRenderer: function(mode, expressionString, state){
// Pre-process the expression.
var exprData = core.expression.parse(expressionString);
if(!(exprData instanceof expression.Helper) && !(exprData instanceof expression.Call)) {
exprData = new expression.Helper(exprData,[],{});
}
// A branching renderer takes truthy and falsey renderer.
return function branchRenderer(scope, options, parentSectionNodeList, truthyRenderer, falseyRenderer){
var nodeList = [this];
nodeList.expression = expressionString;
// register this nodeList.
// Regsiter it with its parent ONLY if this is directly nested. Otherwise, it's unencessary.
nodeLists.register(nodeList, null, parentSectionNodeList || true, state.directlyNested);
// Get the evaluator. This does not need to be cached (probably) because if there
// an observable value, it will be handled by `can.view.live`.
var evaluator = makeEvaluator( scope, options, nodeList, mode, exprData, truthyRenderer, falseyRenderer,
// If this is within a tag, make sure we only get string values.
state.tag );
// Create a compute that can not be observed by other
// comptues. This is important because this renderer is likely called by
// parent expresions. If this value changes, the parent expressions should
// not re-evaluate. We prevent that by making sure this compute is ignored by
// everyone else.
//var compute = can.compute(evaluator, null, false);
var gotCompute = evaluator.isComputed,
compute;
if(gotCompute) {
compute = evaluator;
} else {
compute = can.compute(evaluator, null, false);
}
compute.computeInstance.setPrimaryDepth(nodeList.nesting);
// Bind on the compute to set the cached value. This helps performance
// so live binding can read a cached value instead of re-calculating.
compute.computeInstance.bind("change", k);
var value = compute();
// If value is a function, it's a helper that returned a function.
if(typeof value === "function") {
// A helper function should do it's own binding. Similar to how
// we prevented this function's compute from being noticed by parent expressions,
// we hide any observables read in the function by saving any observables that
// have been read and then setting them back which overwrites any `can.__observe` calls
// performed in value.
can.__notObserve(value)(this);
}
// If the compute has observable dependencies, setup live binding.
else if(gotCompute || compute.computeInstance.hasDependencies ) {
// Depending on where the template is, setup live-binding differently.
if(state.attr) {
live.simpleAttribute(this, state.attr, compute);
}
else if( state.tag ) {
live.attributes( this, compute );
}
else if(state.text && typeof value !== "object"){
live.text(this, compute, this.parentNode, nodeList);
}
else {
live.html(this, compute, this.parentNode, nodeList);
}
}
// If the compute has no observable dependencies, just set the value on the element.
else {
if(state.attr) {
can.attr.set(this, state.attr, value);
}
else if(state.tag) {
live.setAttributes(this, value);
}
else if(state.text && typeof value === "string") {
this.nodeValue = value;
}
else if( value != null ){
elements.replace([this], can.frag(value, this.ownerDocument));
}
}
// Unbind the compute.
compute.computeInstance.unbind("change", k);
};
},
// ## mustacheCore.splitModeFromExpression
// Returns the mustache mode split from the rest of the expression.
/**
* @hide
* Returns the mustache mode split from the rest of the expression.
* @param {can.mustache.Expression} expression
* @param {Object} state The state of HTML where the expression was found.
*/
splitModeFromExpression: function(expression, state){
expression = can.trim(expression);
var mode = expression.charAt(0);
if( "#/{&^>!".indexOf(mode) >= 0 ) {
expression = can.trim( expression.substr(1) );
} else {
mode = null;
}
// Triple braces do nothing within a tag.
if(mode === "{" && state.node) {
mode = null;
}
return {
mode: mode,
expression: expression
};
},
// ## mustacheCore.cleanLineEndings
// Removes line breaks accoding to the mustache specification.
/**
* @hide
* Prunes line breaks accoding to the mustache specification.
* @param {String} template
* @return {String}
*/
cleanLineEndings: function(template){
// Finds mustache tags with space around them or no space around them.
return template.replace( mustacheLineBreakRegExp,
function(whole,
returnBefore,
spaceBefore,
special,
expression,
spaceAfter,
returnAfter,
// A mustache magic tag that has no space around it.
spaceLessSpecial,
spaceLessExpression,
matchIndex){
// IE 8 will provide undefined
spaceAfter = (spaceAfter || "");
returnBefore = (returnBefore || "");
spaceBefore = (spaceBefore || "");
var modeAndExpression = splitModeFromExpression(expression || spaceLessExpression,{});
// If it's a partial or tripple stache, leave in place.
if(spaceLessSpecial || ">{".indexOf( modeAndExpression.mode) >= 0) {
return whole;
} else if( "^#!/".indexOf( modeAndExpression.mode ) >= 0 ) {
// Return the magic tag and a trailing linebreak if this did not
// start a new line and there was an end line.
return special+( matchIndex !== 0 && returnAfter.length ? returnBefore+"\n" :"");
} else {
// There is no mode, return special with spaces around it.
return spaceBefore+special+spaceAfter+(spaceBefore.length || matchIndex !== 0 ? returnBefore+"\n" : "");
}
});
},
Options: utils.Options
};
// ## Local Variable Cache
//
// The following creates slightly more quickly accessible references of the following
// core functions.
var makeEvaluator = core.makeEvaluator,
splitModeFromExpression = core.splitModeFromExpression;
can.view.mustacheCore = core;
return core;
});