UNPKG

can

Version:

MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.

566 lines (565 loc) 19.9 kB
/*! * 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;