can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
566 lines (565 loc) • 19.9 kB
JavaScript
/*!
* CanJS - 2.3.34
* http://canjs.com/
* Copyright (c) 2018 Bitovi
* Mon, 30 Apr 2018 20:56:51 GMT
* Licensed MIT
*/
/*can@2.3.34#view/stache/expression*/
var can = require('../../util/util.js');
var utils = require('./utils.js');
var mustacheHelpers = require('./mustache_helpers.js');
var Scope = require('../scope/scope.js');
var getKeyComputeData = function (key, scope, readOptions) {
var data = scope.computeData(key, readOptions);
can.compute.temporarilyBind(data.compute);
return data;
}, lookupValue = function (key, scope, helperOptions, readOptions) {
var computeData = getKeyComputeData(key, scope, readOptions);
if (!computeData.compute.computeInstance.hasDependencies) {
return {
value: computeData.initialValue,
computeData: computeData
};
} else {
return {
value: computeData.compute,
computeData: computeData
};
}
}, lookupValueOrHelper = function (key, scope, helperOptions, readOptions) {
var res = lookupValue(key, scope, helperOptions, readOptions);
if (res.computeData.initialValue === undefined) {
if (key.charAt(0) === '@' && key !== '@index') {
key = key.substr(1);
}
var helper = mustacheHelpers.getHelper(key, helperOptions);
res.helper = helper && helper.fn;
}
return res;
}, convertToArgExpression = function (expr) {
if (!(expr instanceof Arg) && !(expr instanceof Literal) && !(expr instanceof Hashes)) {
return new Arg(expr);
} else {
return expr;
}
};
var Literal = function (value) {
this._value = value;
};
Literal.prototype.value = function () {
return this._value;
};
var Lookup = function (key, root) {
this.key = key;
this.rootExpr = root;
};
Lookup.prototype.value = function (scope, helperOptions) {
var result = lookupValueOrHelper(this.key, scope, helperOptions);
this.isHelper = result.helper && !result.helper.callAsMethod;
return result.helper || result.value;
};
var ScopeLookup = function (key, root) {
Lookup.apply(this, arguments);
};
ScopeLookup.prototype.value = function (scope, helperOptions) {
return lookupValue(this.key, scope, helperOptions).value;
};
var Arg = function (expression, modifiers) {
this.expr = expression;
this.modifiers = modifiers || {};
this.isCompute = false;
};
Arg.prototype.value = function () {
return this.expr.value.apply(this.expr, arguments);
};
var Hashes = function (hashExpressions) {
this.hashExprs = hashExpressions;
};
Hashes.prototype.value = function () {
var hash = {};
for (var prop in this.hashExprs) {
var val = this.hashExprs[prop], value = val.value.apply(val, arguments);
hash[prop] = {
call: value && value.isComputed && (!val.modifiers || !val.modifiers.compute),
value: value
};
}
return can.compute(function () {
var finalHash = {};
for (var prop in hash) {
finalHash[prop] = hash[prop].call ? hash[prop].value() : hash[prop].value;
}
return finalHash;
});
};
var Call = function (methodExpression, argExpressions, hashes) {
if (hashes && !can.isEmptyObject(hashes)) {
argExpressions.push(new Hashes(hashes));
}
this.methodExpr = methodExpression;
this.argExprs = can.map(argExpressions, convertToArgExpression);
};
Call.prototype.args = function (scope, helperOptions) {
var args = [];
for (var i = 0, len = this.argExprs.length; i < len; i++) {
var arg = this.argExprs[i];
var value = arg.value.apply(arg, arguments);
args.push({
call: value && value.isComputed && (!arg.modifiers || !arg.modifiers.compute),
value: value
});
}
return function () {
var finalArgs = [];
for (var i = 0, len = args.length; i < len; i++) {
finalArgs[i] = args[i].call ? args[i].value() : args[i].value;
}
return finalArgs;
};
};
Call.prototype.value = function (scope, helperScope, helperOptions) {
var method = this.methodExpr.value(scope, helperScope);
this.isHelper = this.methodExpr.isHelper;
var getArgs = this.args(scope, helperScope);
return can.compute(function (newVal) {
var func = method;
if (func && func.isComputed) {
func = func();
}
if (typeof func === 'function') {
var args = getArgs();
if (helperOptions) {
args.push(helperOptions);
}
if (arguments.length) {
args.unshift(new expression.SetIdentifier(newVal));
}
return func.apply(null, args);
}
});
};
var HelperLookup = function () {
Lookup.apply(this, arguments);
};
HelperLookup.prototype.value = function (scope, helperOptions) {
var result = lookupValueOrHelper(this.key, scope, helperOptions, {
isArgument: true,
args: [
scope.attr('.'),
scope
]
});
return result.helper || result.value;
};
var HelperScopeLookup = function () {
Lookup.apply(this, arguments);
};
HelperScopeLookup.prototype.value = function (scope, helperOptions) {
return lookupValue(this.key, scope, helperOptions, {
callMethodsOnObservables: true,
isArgument: true,
args: [
scope.attr('.'),
scope
]
}).value;
};
var Helper = function (methodExpression, argExpressions, hashExpressions) {
this.methodExpr = methodExpression;
this.argExprs = argExpressions;
this.hashExprs = hashExpressions;
this.mode = null;
};
Helper.prototype.args = function (scope, helperOptions) {
var args = [];
for (var i = 0, len = this.argExprs.length; i < len; i++) {
var arg = this.argExprs[i];
args.push(arg.value.apply(arg, arguments));
}
return args;
};
Helper.prototype.hash = function (scope, helperOptions) {
var hash = {};
for (var prop in this.hashExprs) {
var val = this.hashExprs[prop];
hash[prop] = val.value.apply(val, arguments);
}
return hash;
};
Helper.prototype.helperAndValue = function (scope, helperOptions) {
var looksLikeAHelper = this.argExprs.length || !can.isEmptyObject(this.hashExprs), helper, value, methodKey = this.methodExpr instanceof Literal ? '' + this.methodExpr._value : this.methodExpr.key, initialValue, args;
if (looksLikeAHelper) {
helper = mustacheHelpers.getHelper(methodKey, helperOptions);
var context = scope.attr('.');
if (!helper && typeof context[methodKey] === 'function') {
helper = { fn: context[methodKey] };
}
}
if (!helper) {
args = this.args(scope, helperOptions);
var computeData = getKeyComputeData(methodKey, scope, {
isArgument: false,
args: args && args.length ? args : [
scope.attr('.'),
scope
]
}), compute = computeData.compute;
initialValue = computeData.initialValue;
if (computeData.compute.computeInstance.hasDependencies) {
value = compute;
} else {
value = initialValue;
}
if (!looksLikeAHelper && initialValue === undefined) {
helper = mustacheHelpers.getHelper(methodKey, helperOptions);
}
}
return {
value: value,
args: args,
helper: helper && helper.fn
};
};
Helper.prototype.evaluator = function (helper, scope, helperOptions, readOptions, nodeList, truthyRenderer, falseyRenderer, stringOnly) {
var helperOptionArg = {
fn: function () {
},
inverse: function () {
},
stringOnly: stringOnly
}, context = scope.attr('.'), args = this.args(scope, helperOptions, nodeList, truthyRenderer, falseyRenderer, stringOnly), hash = this.hash(scope, helperOptions, nodeList, truthyRenderer, falseyRenderer, stringOnly);
utils.convertToScopes(helperOptionArg, scope, helperOptions, nodeList, truthyRenderer, falseyRenderer, stringOnly);
can.simpleExtend(helperOptionArg, {
context: context,
scope: scope,
contexts: scope,
hash: hash,
nodeList: nodeList,
exprData: this,
helperOptions: helperOptions,
helpers: helperOptions
});
args.push(helperOptionArg);
return function () {
return helper.apply(context, args);
};
};
Helper.prototype.value = function (scope, helperOptions, nodeList, truthyRenderer, falseyRenderer, stringOnly) {
var helperAndValue = this.helperAndValue(scope, helperOptions);
var helper = helperAndValue.helper;
if (!helper) {
return helperAndValue.value;
}
var fn = this.evaluator(helper, scope, helperOptions, nodeList, truthyRenderer, falseyRenderer, stringOnly);
var compute = can.compute(fn);
can.compute.temporarilyBind(compute);
if (!compute.computeInstance.hasDependencies) {
return compute();
} else {
return compute;
}
};
var keyRegExp = /[\w\.\\\-_@\/\&%]+/, tokensRegExp = /('.*?'|".*?"|=|[\w\.\\\-_@\/*%\$:]+|[\(\)]|,|\~)/g, literalRegExp = /^('.*?'|".*?"|[0-9]+\.?[0-9]*|true|false|null|undefined)$/;
var isTokenKey = function (token) {
return keyRegExp.test(token);
};
var testDot = /^[\.@]\w/;
var isAddingToExpression = function (token) {
return isTokenKey(token) && testDot.test(token);
};
var ensureChildren = function (type) {
if (!type.children) {
type.children = [];
}
return type;
};
var Stack = function () {
this.root = {
children: [],
type: 'Root'
};
this.current = this.root;
this.stack = [this.root];
};
can.simpleExtend(Stack.prototype, {
top: function () {
return can.last(this.stack);
},
isRootTop: function () {
return this.top() === this.root;
},
popTo: function (types) {
this.popUntil(types);
if (!this.isRootTop()) {
this.stack.pop();
}
},
firstParent: function (types) {
var curIndex = this.stack.length - 2;
while (curIndex > 0 && can.inArray(this.stack[curIndex].type, types) === -1) {
curIndex--;
}
return this.stack[curIndex];
},
popUntil: function (types) {
while (can.inArray(this.top().type, types) === -1 && !this.isRootTop()) {
this.stack.pop();
}
return this.top();
},
addTo: function (types, type) {
var cur = this.popUntil(types);
ensureChildren(cur).children.push(type);
},
addToAndPush: function (types, type) {
this.addTo(types, type);
this.stack.push(type);
},
topLastChild: function () {
return can.last(this.top().children);
},
replaceTopLastChild: function (type) {
var children = ensureChildren(this.top()).children;
children.pop();
children.push(type);
return type;
},
replaceTopLastChildAndPush: function (type) {
this.replaceTopLastChild(type);
this.stack.push(type);
},
replaceTopAndPush: function (type) {
var children;
if (this.top() === this.root) {
children = ensureChildren(this.top()).children;
} else {
this.stack.pop();
children = ensureChildren(this.top()).children;
}
children.pop();
children.push(type);
this.stack.push(type);
return type;
}
});
var convertKeyToLookup = function (key) {
var lastPath = key.lastIndexOf('./');
var lastDot = key.lastIndexOf('.');
if (lastDot > lastPath) {
return key.substr(0, lastDot) + '@' + key.substr(lastDot + 1);
}
var firstNonPathCharIndex = lastPath === -1 ? 0 : lastPath + 2;
var firstNonPathChar = key.charAt(firstNonPathCharIndex);
if (firstNonPathChar === '.' || firstNonPathChar === '@') {
return key.substr(0, firstNonPathCharIndex) + '@' + key.substr(firstNonPathCharIndex + 1);
} else {
return key.substr(0, firstNonPathCharIndex) + '@' + key.substr(firstNonPathCharIndex);
}
};
var convertToAtLookup = function (ast) {
if (ast.type === 'Lookup') {
ast.key = convertKeyToLookup(ast.key);
}
return ast;
};
var convertToHelperIfTopIsLookup = function (stack) {
var top = stack.top();
if (top && top.type === 'Lookup') {
var base = stack.stack[stack.stack.length - 2];
if (base.type !== 'Helper' && base) {
stack.replaceTopAndPush({
type: 'Helper',
method: top
});
}
}
};
var expression = {
convertKeyToLookup: convertKeyToLookup,
Literal: Literal,
Lookup: Lookup,
ScopeLookup: ScopeLookup,
Arg: Arg,
Hashes: Hashes,
Call: Call,
Helper: Helper,
HelperLookup: HelperLookup,
HelperScopeLookup: HelperScopeLookup,
SetIdentifier: function (value) {
this.value = value;
},
tokenize: function (expression) {
var tokens = [];
(can.trim(expression) + ' ').replace(tokensRegExp, function (whole, arg) {
tokens.push(arg);
});
return tokens;
},
lookupRules: {
'default': function (ast, methodType, isArg) {
var name = (methodType === 'Helper' && !ast.root ? 'Helper' : '') + (isArg ? 'Scope' : '') + 'Lookup';
return expression[name];
},
'method': function (ast, methodType, isArg) {
return ScopeLookup;
}
},
methodRules: {
'default': function (ast) {
return ast.type === 'Call' ? Call : Helper;
},
'call': function (ast) {
return Call;
}
},
parse: function (expressionString, options) {
options = options || {};
var ast = this.ast(expressionString);
if (!options.lookupRule) {
options.lookupRule = 'default';
}
if (typeof options.lookupRule === 'string') {
options.lookupRule = expression.lookupRules[options.lookupRule];
}
if (!options.methodRule) {
options.methodRule = 'default';
}
if (typeof options.methodRule === 'string') {
options.methodRule = expression.methodRules[options.methodRule];
}
var expr = this.hydrateAst(ast, options, options.baseMethodType || 'Helper');
return expr;
},
hydrateAst: function (ast, options, methodType, isArg) {
var hashes, self = this;
if (ast.type === 'Lookup') {
return new (options.lookupRule(ast, methodType, isArg))(ast.key, ast.root && this.hydrateAst(ast.root, options, methodType));
} else if (ast.type === 'Literal') {
return new Literal(ast.value);
} else if (ast.type === 'Arg') {
return new Arg(this.hydrateAst(ast.children[0], options, methodType, isArg), { compute: true });
} else if (ast.type === 'Hashes') {
hashes = {};
can.each(ast.children, function (child) {
hashes[child.prop] = self.hydrateAst(child.children[0], options, ast.type, true);
});
return new Hashes(hashes);
} else if (ast.type === 'Hash') {
throw new Error('');
} else if (ast.type === 'Call' || ast.type === 'Helper') {
var args = [];
hashes = {};
can.each(ast.children, function (child) {
if (child.type === 'Hash') {
hashes[child.prop] = self.hydrateAst(child.children[0], options, ast.type, true);
} else {
args.push(self.hydrateAst(child, options, ast.type, true));
}
});
return new (options.methodRule(ast))(this.hydrateAst(ast.method, options, ast.type), args, hashes);
}
},
ast: function (expression) {
var tokens = this.tokenize(expression);
return this.parseAst(tokens, { index: 0 });
},
parseAst: function (tokens, cursor) {
var stack = new Stack(), top;
while (cursor.index < tokens.length) {
var token = tokens[cursor.index], nextToken = tokens[cursor.index + 1];
cursor.index++;
if (literalRegExp.test(token)) {
convertToHelperIfTopIsLookup(stack);
stack.addTo([
'Helper',
'Call',
'Hash'
], {
type: 'Literal',
value: utils.jsonParse(token)
});
} else if (nextToken === '=') {
top = stack.top();
if (top && top.type === 'Lookup') {
var firstParent = stack.firstParent([
'Call',
'Helper',
'Hash'
]);
if (firstParent.type === 'Call' || firstParent.type === 'Root') {
stack.popUntil(['Call']);
top = stack.top();
stack.replaceTopAndPush({
type: 'Helper',
method: top.type === 'Root' ? can.last(top.children) : top
});
}
}
top = stack.popUntil([
'Helper',
'Call',
'Hashes'
]);
if (top.type === 'Call') {
stack.addToAndPush(['Call'], { type: 'Hashes' });
}
stack.addToAndPush([
'Helper',
'Hashes'
], {
type: 'Hash',
prop: token
});
cursor.index++;
} else if (keyRegExp.test(token)) {
var last = stack.topLastChild();
if (last && last.type === 'Call' && isAddingToExpression(token)) {
stack.replaceTopLastChildAndPush({
type: 'Lookup',
root: last,
key: token
});
} else {
convertToHelperIfTopIsLookup(stack);
stack.addToAndPush([
'Helper',
'Call',
'Hash',
'Arg'
], {
type: 'Lookup',
key: token
});
}
} else if (token === '~') {
convertToHelperIfTopIsLookup(stack);
stack.addToAndPush([
'Helper',
'Call',
'Hash'
], {
type: 'Arg',
key: token
});
} else if (token === '(') {
top = stack.top();
if (top.type === 'Lookup') {
stack.replaceTopAndPush({
type: 'Call',
method: convertToAtLookup(top)
});
} else {
throw new Error('Unable to understand expression ' + tokens.join(''));
}
} else if (token === ')') {
stack.popTo(['Call']);
} else if (token === ',') {
stack.popUntil(['Call']);
}
}
return stack.root.children[0];
}
};
can.expression = expression;
module.exports = expression;