can-stache
Version:
Live binding handlebars templates
522 lines (468 loc) • 18.8 kB
JavaScript
//
// 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.
var live = require('can-view-live');
var liveHelpers = require("can-view-live/lib/helpers");
var Observation = require('can-observation');
var ObservationRecorder = require('can-observation-recorder');
var utils = require('./utils');
var expression = require('./expression');
var frag = require("can-fragment");
var domMutate = require("can-dom-mutate");
var canSymbol = require("can-symbol");
var canReflect = require("can-reflect");
var dev = require("can-log/dev/dev");
var getDocument = require("can-globals/document/document");
var defineLazyValue = require("can-define-lazy-value");
var toDOMSymbol = canSymbol.for("can.toDOM");
// Lazily lookup the context only if it's needed.
function HelperOptions(scope, exprData, stringOnly) {
this.metadata = { rendered: false };
this.stringOnly = stringOnly;
this.scope = scope;
this.exprData = exprData;
}
defineLazyValue(HelperOptions.prototype,"context", function(){
return this.scope.peek("this");
});
// ## Helpers
var mustacheLineBreakRegExp = /(?:(^|\r?\n)(\s*)(\{\{([\s\S]*)\}\}\}?)([^\S\n\r]*)($|\r?\n))|(\{\{([\s\S]*)\}\}\}?)/g,
mustacheWhitespaceRegExp = /\s*\{\{--\}\}\s*|\s*(\{\{\{?)-|-(\}\}\}?)\s*/g,
k = function(){};
var viewInsertSymbol = canSymbol.for("can.viewInsert");
// DOM, safeString or the insertSymbol can opt-out of updating as text
function valueShouldBeInsertedAsHTML(value) {
return value !== null && typeof value === "object" && (
typeof value[toDOMSymbol] === "function" ||
typeof value[viewInsertSymbol] === "function" ||
typeof value.nodeType === "number" );
}
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 expression 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, mode, exprData, truthyRenderer, falseyRenderer, stringOnly) {
if(mode === "^") {
var temp = truthyRenderer;
truthyRenderer = falseyRenderer;
falseyRenderer = temp;
}
var value,
helperOptions = new HelperOptions(scope , exprData, stringOnly);
// set up renderers
utils.createRenderers(helperOptions, scope ,truthyRenderer, falseyRenderer, stringOnly);
if(exprData instanceof expression.Call) {
value = exprData.value(scope, helperOptions);
} else if (exprData instanceof expression.Bracket) {
value = exprData.value(scope);
} else if (exprData instanceof expression.Lookup) {
value = exprData.value(scope);
} else if (exprData instanceof expression.Literal) {
value = exprData.value.bind(exprData);
} else if (exprData instanceof expression.Helper && exprData.methodExpr instanceof expression.Bracket) {
// Brackets get wrapped in Helpers when used in attributes
// like `<p class="{{ foo[bar] }}" />`
value = exprData.methodExpr.value(scope, helperOptions);
} else {
value = exprData.value(scope, helperOptions);
if (typeof value === "function") {
return value;
}
}
// {{#something()}}foo{{/something}}
// return evaluator for no mode or rendered value if a renderer was called
if(!mode || helperOptions.metadata.rendered) {
return value;
} else if( mode === "#" || mode === "^" ) {
return function(){
// Get the value
var finalValue = canReflect.getValue(value);
var result;
// if options.fn or options.inverse was called, we take the observable's return value
// as what should be put in the DOM.
if(helperOptions.metadata.rendered) {
result = finalValue;
}
// If it's an array, render.
else if ( typeof finalValue !== "string" && canReflect.isListLike(finalValue) ) {
var isObserveList = canReflect.isObservableLike(finalValue) &&
canReflect.isListLike(finalValue);
if(canReflect.getKeyValue(finalValue, "length")) {
if (stringOnly) {
result = utils.getItemsStringContent(finalValue, isObserveList, helperOptions);
} else {
result = frag(utils.getItemsFragContent(finalValue, helperOptions, scope));
}
} else {
result = helperOptions.inverse(scope);
}
}
else {
result = finalValue ? helperOptions.fn(finalValue || scope) : helperOptions.inverse(scope);
}
// We always set the rendered result back to false.
// - Future calls might change from returning a value to calling `.fn`
// - We are calling `.fn` and `.inverse` ourselves.
helperOptions.metadata.rendered = false;
return result;
};
} 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} expressionString
* @param {Object} state The html state of where the expression was found.
* @return {function(this:HTMLElement,can-view-scope,can.view.Options)} A renderer function
* live binds a partial.
*/
makeLiveBindingPartialRenderer: function(expressionString, state){
expressionString = expressionString.trim();
var exprData,
partialName = expressionString.split(/\s+/).shift();
if(partialName !== expressionString) {
exprData = core.expression.parse(expressionString);
}
return function(scope){
//!steal-remove-start
if (process.env.NODE_ENV !== 'production') {
scope.set('scope.filename', state.filename);
scope.set('scope.lineNumber', state.lineNo);
}
//!steal-remove-end
var partialFrag = new Observation(function(){
var localPartialName = partialName;
var partialScope = scope;
// If the second parameter of a partial is a custom context
if(exprData && exprData.argExprs.length === 1) {
var newContext = canReflect.getValue( exprData.argExprs[0].value(scope) );
if(typeof newContext === "undefined") {
//!steal-remove-start
if (process.env.NODE_ENV !== 'production') {
dev.warn('The context ('+ exprData.argExprs[0].key +') you passed into the' +
'partial ('+ partialName +') is not defined in the scope!');
}
//!steal-remove-end
}else{
partialScope = scope.add(newContext);
}
}
// Look up partials in templateContext first
var partial = canReflect.getKeyValue(partialScope.templateContext.partials, localPartialName);
var renderer;
if (partial) {
renderer = function() {
return partial.render ? partial.render(partialScope)
: partial(partialScope);
};
}
// Use can.view to get and render the partial.
else {
var scopePartialName = partialScope.read(localPartialName, {
isArgument: true
}).value;
if (scopePartialName === null || !scopePartialName && localPartialName[0] === '*') {
return frag("");
}
if (scopePartialName) {
localPartialName = scopePartialName;
}
renderer = function() {
if(typeof localPartialName === "function"){
return localPartialName(partialScope, {});
} else {
var domRenderer = core.getTemplateById(localPartialName);
//!steal-remove-start
if (process.env.NODE_ENV !== 'production') {
if (!domRenderer) {
dev.warn(
(state.filename ? state.filename + ':' : '') +
(state.lineNo ? state.lineNo + ': ' : '') +
'Unable to find partial "' + localPartialName + '".');
}
}
//!steal-remove-end
return domRenderer ? domRenderer(partialScope, {}) : getDocument().createDocumentFragment();
}
};
}
var res = ObservationRecorder.ignore(renderer)();
return frag(res);
});
live.html(this, partialFrag);
};
},
// ## 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.stache.Expression} expression
* @param {Object} state The html state of where the expression was found.
* @return {function(can.view.Scope,can.view.Options, can-stache.view, can.view.renderer)}
*/
makeStringBranchRenderer: function(mode, expressionString, state){
var exprData = core.expression.parse(expressionString),
// Use the full mustache expression as the cache key.
fullExpression = mode+expressionString;
// A branching renderer takes truthy and falsey renderer.
var branchRenderer = function branchRenderer(scope, truthyRenderer, falseyRenderer){
//!steal-remove-start
if (process.env.NODE_ENV !== 'production') {
scope.set('scope.filename', state.filename);
scope.set('scope.lineNumber', state.lineNo);
}
//!steal-remove-end
// Check the scope's cache if the evaluator already exists for performance.
var evaluator = scope.__cache[fullExpression];
if(mode || !evaluator) {
evaluator = makeEvaluator( scope, mode, exprData, truthyRenderer, falseyRenderer, true);
if(!mode) {
scope.__cache[fullExpression] = evaluator;
}
}
var gotObservableValue = evaluator[canSymbol.for("can.onValue")],
res;
// Run the evaluator and return the result.
if(gotObservableValue) {
res = canReflect.getValue(evaluator);
} else {
res = evaluator();
}
if (res == null) {
return "";
}
return res.nodeType === 11 ? res.textContent : ""+res;
};
branchRenderer.exprData = exprData;
return branchRenderer;
},
// ## 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.stache.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);
// A branching renderer takes truthy and falsey renderer.
var branchRenderer = function branchRenderer(scope, truthyRenderer, falseyRenderer){
// If this is within a tag, make sure we only get string values.
var stringOnly = state.tag;
//!steal-remove-start
if (process.env.NODE_ENV !== 'production') {
scope.set('scope.filename', state.filename);
scope.set('scope.lineNumber', state.lineNo);
}
//!steal-remove-end
// 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, mode, exprData, truthyRenderer, falseyRenderer, stringOnly );
// Create a compute that can not be observed by other
// computes. This is important because this renderer is likely called by
// parent expressions. 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 gotObservableValue = evaluator[canSymbol.for("can.onValue")];
var observable;
if(gotObservableValue) {
observable = evaluator;
} else {
//!steal-remove-start
if (process.env.NODE_ENV !== 'production') {
Object.defineProperty(evaluator,"name",{
value: "{{"+(mode || "")+expressionString+"}}"
});
}
//!steal-remove-end
observable = new Observation(evaluator,null,{isObservable: false});
}
// Bind on the computeValue to set the cached value. This helps performance
// so live binding can read a cached value instead of re-calculating.
canReflect.onValue(observable, k);
var value = canReflect.getValue(observable);
// If value is a function and not a Lookup ({{foo}}),
// it's a helper that returned a function and should be called.
if(typeof value === "function" && !(exprData instanceof expression.Lookup)) {
// 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.
ObservationRecorder.ignore(value)(this);
}
// If the computeValue has observable dependencies, setup live binding.
else if( canReflect.valueHasDependencies(observable) ) {
// Depending on where the template is, setup live-binding differently.
if(state.attr) {
live.attr(this, state.attr, observable);
}
else if( state.tag ) {
live.attrs( this, observable );
}
else if(state.text && !valueShouldBeInsertedAsHTML(value)) {
//!steal-remove-start
if (process.env.NODE_ENV !== 'production') {
if(value !== null && typeof value === "object") {
dev.warn("Previously, the result of "+
expressionString+" in "+state.filename+":"+state.lineNo+
", was being inserted as HTML instead of TEXT. Please use stache.safeString(obj) "+
"if you would like the object to be treated as HTML.");
}
}
//!steal-remove-end
live.text(this, observable);
} else {
live.html(this, observable);
}
}
// If the computeValue has no observable dependencies, just set the value on the element.
else {
if(state.attr) {
domMutate.setAttribute(this, state.attr, value);
}
else if(state.tag) {
live.attrs(this, value);
}
else if(state.text && !valueShouldBeInsertedAsHTML(value)) {
this.nodeValue = liveHelpers.makeString(value);
}
else if( value != null ){
if (typeof value[viewInsertSymbol] === "function") {
var insert = value[viewInsertSymbol]({});
this.parentNode.replaceChild( insert, this );
} else {
this.parentNode.replaceChild(frag(value, this.ownerDocument), this);
//domMutateNode.replaceChild.call(this.parentNode, frag(value, this.ownerDocument), this);
}
}
}
// Unbind the compute.
canReflect.offValue(observable, k);
};
branchRenderer.exprData = exprData;
return branchRenderer;
},
// ## 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.stache.Expression} expression
* @param {Object} state The state of HTML where the expression was found.
*/
splitModeFromExpression: function(expression, state){
expression = expression.trim();
var mode = expression.charAt(0);
if( "#/{&^>!<".indexOf(mode) >= 0 ) {
expression = expression.substr(1).trim();
} 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.
// Add a normalized leading space, if there was any leading space, in case this abuts a tag name
spaceBefore = (returnBefore + spaceBefore) && " ";
return spaceBefore+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" : "");
}
});
},
// ## mustacheCore.cleanWhitespaceControl
// Removes whitespace according to the whitespace control.
/**
* @hide
* Prunes whitespace according to the whitespace control.
* @param {String} template
* @return {String}
*/
cleanWhitespaceControl: function(template) {
return template.replace(mustacheWhitespaceRegExp, "$1$2");
},
getTemplateById: function(){}
};
// ## Local Variable Cache
//
// The following creates slightly more quickly accessible references of the following
// core functions.
var makeEvaluator = core.makeEvaluator,
splitModeFromExpression = core.splitModeFromExpression;
module.exports = core;
;