UNPKG

can-stache

Version:

Live binding handlebars templates

521 lines (479 loc) 15.3 kB
"use strict"; // ## Expression Types // // These expression types return a value. They are assembled by `expression.parse`. var Arg = require("../expressions/arg"); var Literal = require("../expressions/literal"); var Hashes = require("../expressions/hashes"); var Bracket = require("../expressions/bracket"); var Call = require("../expressions/call"); var Helper = require("../expressions/helper"); var Lookup = require("../expressions/lookup"); var SetIdentifier = require("./set-identifier"); var expressionHelpers = require("../src/expression-helpers"); var utils = require('./utils'); var assign = require('can-assign'); var last = utils.last; var canReflect = require("can-reflect"); var canSymbol = require("can-symbol"); var sourceTextSymbol = canSymbol.for("can-stache.sourceText"); // ### Hash // A placeholder. This isn't actually used. var Hash = function(){ }; // jshint ignore:line // NAME - \w // KEY - foo, foo.bar, foo@bar, %foo (special), &foo (references), ../foo, ./foo // ARG - ~KEY, KEY, CALLEXPRESSION, PRIMITIVE // CALLEXPRESSION = KEY(ARG,ARG, NAME=ARG) // HELPEREXPRESSION = KEY ARG ARG NAME=ARG // DOT .NAME // AT @NAME // var keyRegExp = /[\w\.\\\-_@\/\&%]+/, tokensRegExp = /('.*?'|".*?"|=|[\w\.\\\-_@\/*%\$]+|[\(\)]|,|\~|\[|\]\s*|\s*(?=\[))/g, bracketSpaceRegExp = /\]\s+/, 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]; }; assign(Stack.prototype,{ top: function(){ return last(this.stack); }, isRootTop: function(){ return this.top() === this.root; }, popTo: function(types){ this.popUntil(types); this.pop(); }, pop: function() { if(!this.isRootTop()) { this.stack.pop(); } }, first: function(types){ var curIndex = this.stack.length - 1; while( curIndex > 0 && types.indexOf(this.stack[curIndex].type) === -1 ) { curIndex--; } return this.stack[curIndex]; }, firstParent: function(types){ var curIndex = this.stack.length - 2; while( curIndex > 0 && types.indexOf(this.stack[curIndex].type) === -1 ) { curIndex--; } return this.stack[curIndex]; }, popUntil: function(types){ while( types.indexOf(this.top().type) === -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); }, push: function(type) { this.stack.push(type); }, topLastChild: function(){ return 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(); // get parent and clean children = ensureChildren(this.top()).children; } children.pop(); children.push(type); this.stack.push(type); return type; } }); // converts // - "../foo" -> "../@foo", // - "foo" -> "@foo", // - ".foo" -> "@foo", // - "./foo" -> "./@foo" // - "foo.bar" -> "foo@bar" 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") { canReflect.setKeyValue(ast, sourceTextSymbol, ast.key); ast.key = convertKeyToLookup(ast.key); } return ast; }; var convertToHelperIfTopIsLookup = function(stack){ var top = stack.top(); // if two scopes, that means a helper if(top && top.type === "Lookup") { var base = stack.stack[stack.stack.length - 2]; // That lookup shouldn't be part of a Helper already or if(base.type !== "Helper" && base) { stack.replaceTopAndPush({ type: "Helper", method: top }); } } }; var expression = { toComputeOrValue: expressionHelpers.toComputeOrValue, convertKeyToLookup: convertKeyToLookup, Literal: Literal, Lookup: Lookup, Arg: Arg, Hash: Hash, Hashes: Hashes, Call: Call, Helper: Helper, Bracket: Bracket, SetIdentifier: SetIdentifier, tokenize: function(expression){ var tokens = []; (expression.trim() + ' ').replace(tokensRegExp, function (whole, arg) { if (bracketSpaceRegExp.test(arg)) { tokens.push(arg[0]); tokens.push(arg.slice(1)); } else { tokens.push(arg); } }); return tokens; }, lookupRules: { "default": function(ast, methodType, isArg){ return ast.type === "Helper" ? Helper : Lookup; }, "method": function(ast, methodType, isArg){ return Lookup; } }, methodRules: { "default": function(ast){ return ast.type === "Call" ? Call : Helper; }, "call": function(ast){ return Call; } }, // ## expression.parse // // - {String} expressionString - A stache expression like "abc foo()" // - {Object} options // - baseMethodType - Treat this like a Helper or Call. Default to "Helper" // - lookupRule - "default" or "method" // - methodRule - "default" or "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; if(ast.type === "Lookup") { var LookupRule = options.lookupRule(ast, methodType, isArg); var lookup = new LookupRule(ast.key, ast.root && this.hydrateAst(ast.root, options, methodType), ast[sourceTextSymbol] ); return lookup; } 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 === "Hash") { throw new Error(""); } else if(ast.type === "Hashes") { hashes = {}; ast.children.forEach(function(hash){ hashes[hash.prop] = this.hydrateAst( hash.children[0], options, methodType, true ); }, this); return new Hashes(hashes); } else if(ast.type === "Call" || ast.type === "Helper") { //get all arguments and hashes hashes = {}; var args = [], children = ast.children, ExpressionType = options.methodRule(ast); if(children) { for(var i = 0 ; i <children.length; i++) { var child = children[i]; if(child.type === "Hashes" && ast.type === "Helper" && (ExpressionType !== Call)) { child.children.forEach(function(hash){ hashes[hash.prop] = this.hydrateAst( hash.children[0], options, ast.type, true ); }, this); } else { args.push( this.hydrateAst(child, options, ast.type, true) ); } } } return new ExpressionType(this.hydrateAst(ast.method, options, ast.type), args, hashes); } else if (ast.type === "Bracket") { var originalKey; //!steal-remove-start if (process.env.NODE_ENV !== 'production') { originalKey = ast[canSymbol.for("can-stache.originalKey")]; } //!steal-remove-end return new Bracket( this.hydrateAst(ast.children[0], options), ast.root ? this.hydrateAst(ast.root, options) : undefined, originalKey ); } }, ast: function(expression){ var tokens = this.tokenize(expression); return this.parseAst(tokens, { index: 0 }); }, parseAst: function(tokens, cursor) { // jshint maxdepth: 6 var stack = new Stack(), top, firstParent, lastToken; while(cursor.index < tokens.length) { var token = tokens[cursor.index], nextToken = tokens[cursor.index+1]; cursor.index++; // Hash if(nextToken === "=") { //convertToHelperIfTopIsLookup(stack); top = stack.top(); // If top is a Lookup, we might need to convert to a helper. if(top && top.type === "Lookup") { // Check if current Lookup is part of a Call, Helper, or Hash // If it happens to be first within a Call or Root, that means // this is helper syntax. 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" ? last(top.children) : top }); } } firstParent = stack.first(["Call","Helper","Hashes","Root"]); // makes sure we are adding to Hashes if there already is one // otherwise we create one. var hash = {type: "Hash", prop: token}; if(firstParent.type === "Hashes") { stack.addToAndPush(["Hashes"], hash); } else { stack.addToAndPush(["Helper", "Call","Root"], { type: "Hashes", children: [hash] }); stack.push(hash); } cursor.index++; } // Literal else if(literalRegExp.test( token )) { convertToHelperIfTopIsLookup(stack); // only add to hash if there's not already a child. firstParent = stack.first(["Helper", "Call", "Hash", "Bracket"]); if(firstParent.type === "Hash" && (firstParent.children && firstParent.children.length > 0)) { stack.addTo(["Helper", "Call", "Bracket"], {type: "Literal", value: utils.jsonParse( token )}); } else if(firstParent.type === "Bracket" && (firstParent.children && firstParent.children.length > 0)) { stack.addTo(["Helper", "Call", "Hash"], {type: "Literal", value: utils.jsonParse( token )}); } else { stack.addTo(["Helper", "Call", "Hash", "Bracket"], {type: "Literal", value: utils.jsonParse( token )}); } } // Lookup else if(keyRegExp.test(token)) { lastToken = stack.topLastChild(); firstParent = stack.first(["Helper", "Call", "Hash", "Bracket"]); // if we had `foo().bar`, we need to change to a Lookup that looks up from lastToken. if(lastToken && (lastToken.type === "Call" || lastToken.type === "Bracket" ) && isAddingToExpression(token)) { stack.replaceTopLastChildAndPush({ type: "Lookup", root: lastToken, key: token.slice(1) // remove leading `.` }); } else if(firstParent.type === 'Bracket') { // a Bracket expression without children means we have // parsed `foo[` of an expression like `foo[bar]` // so we know to add the Lookup as a child of the Bracket expression if (!(firstParent.children && firstParent.children.length > 0)) { stack.addToAndPush(["Bracket"], {type: "Lookup", key: token}); } else { // check if we are adding to a helper like `eq foo[bar] baz` // but not at the `.baz` of `eq foo[bar].baz xyz` if(stack.first(["Helper", "Call", "Hash", "Arg"]).type === 'Helper' && token[0] !== '.') { stack.addToAndPush(["Helper"], {type: "Lookup", key: token}); } else { // otherwise, handle the `.baz` in expressions like `foo[bar].baz` stack.replaceTopAndPush({ type: "Lookup", key: token.slice(1), root: firstParent }); } } } else { // if two scopes, that means a helper convertToHelperIfTopIsLookup(stack); stack.addToAndPush(["Helper", "Call", "Hash", "Arg", "Bracket"], {type: "Lookup", key: token}); } } // Arg else if(token === "~") { convertToHelperIfTopIsLookup(stack); stack.addToAndPush(["Helper", "Call", "Hash"], {type: "Arg", key: token}); } // Call // foo[bar()] else if(token === "(") { top = stack.top(); lastToken = stack.topLastChild(); if(top.type === "Lookup") { stack.replaceTopAndPush({ type: "Call", method: convertToAtLookup(top) }); // Nested Call // foo()() } else if (lastToken && lastToken.type === "Call") { stack.replaceTopAndPush({ type: "Call", method: lastToken }); } else { throw new Error("Unable to understand expression "+tokens.join('')); } } // End Call else if(token === ")") { stack.popTo(["Call"]); } // End Call argument else if(token === ",") { // The {{let foo=zed, bar=car}} helper is not in a call // expression. var call = stack.first(["Call"]); if(call.type !== "Call") { stack.popUntil(["Hash"]); } else { stack.popUntil(["Call"]); } } // Bracket else if(token === "[") { top = stack.top(); lastToken = stack.topLastChild(); // foo()[bar] => top -> root, lastToken -> {t: call, m: "@foo"} // foo()[bar()] => same as above last thing we see was a call expression "rotate" // test['foo'][0] => lastToken => {root: test, t: Bracket, c: 'foo' } // log(thing['prop'][0]) => // // top -> {Call, children|args: [Bracket(Lookup(thing), c: ['[prop]'])]} // last-> Bracket(Lookup(thing), c: ['[prop]']) if (lastToken && (lastToken.type === "Call" || lastToken.type === "Bracket" ) ) { // must be on top of the stack as it recieves new stuff ... // however, what we really want is to stack.replaceTopLastChildAndPush({type: "Bracket", root: lastToken}); } else if (top.type === "Lookup" || top.type === "Bracket") { var bracket = {type: "Bracket", root: top}; //!steal-remove-start if (process.env.NODE_ENV !== 'production') { canReflect.setKeyValue(bracket, canSymbol.for("can-stache.originalKey"), top.key); } //!steal-remove-end stack.replaceTopAndPush(bracket); } else if (top.type === "Call") { stack.addToAndPush(["Call"], { type: "Bracket" }); } else if (top === " ") { stack.popUntil(["Lookup", "Call"]); convertToHelperIfTopIsLookup(stack); stack.addToAndPush(["Helper", "Call", "Hash"], {type: "Bracket"}); } else { stack.replaceTopAndPush({type: "Bracket"}); } } // End Bracket else if(token === "]") { stack.pop(); } else if(token === " ") { stack.push(token); } } return stack.root.children[0]; } }; module.exports = expression;