var utils = require('./utils'), STR_SAVE_RE = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, STR_RESTORE_RE = /"(\d+)"/g, NEWLINE_RE = /\n/g, CTOR_RE = new RegExp('constructor'.split('').join('[\'"+, ]*')), UNICODE_RE = /\\u\d\d\d\d/ // Variable extraction scooped from https://github.com/RubyLouvre/avalon var KEYWORDS = // keywords 'break,case,catch,continue,debugger,default,delete,do,else,false' + ',finally,for,function,if,in,instanceof,new,null,return,switch,this' + ',throw,true,try,typeof,var,void,while,with,undefined' + // reserved ',abstract,boolean,byte,char,class,const,double,enum,export,extends' + ',final,float,goto,implements,import,int,interface,long,native' + ',package,private,protected,public,short,static,super,synchronized' + ',throws,transient,volatile' + // ECMA 5 - use strict ',arguments,let,yield' + // allow using Math in expressions ',Math', KEYWORDS_RE = new RegExp(["\\b" + KEYWORDS.replace(/,/g, '\\b|\\b') + "\\b"].join('|'), 'g'), REMOVE_RE = /\/\*(?:.|\n)*?\*\/|\/\/[^\n]*\n|\/\/[^\n]*$|'[^']*'|"[^"]*"|[\s\t\n]*\.[\s\t\n]*[$\w\.]+|[\{,]\s*[\w\$_]+\s*:/g, SPLIT_RE = /[^\w$]+/g, NUMBER_RE = /\b\d[^,]*/g, BOUNDARY_RE = /^,+|,+$/g /** * Strip top level variable names from a snippet of JS expression */ function getVariables (code) { code = code .replace(REMOVE_RE, '') .replace(SPLIT_RE, ',') .replace(KEYWORDS_RE, '') .replace(NUMBER_RE, '') .replace(BOUNDARY_RE, '') return code ? code.split(/,+/) : [] } /** * A given path could potentially exist not on the * current compiler, but up in the parent chain somewhere. * This function generates an access relationship string * that can be used in the getter function by walking up * the parent chain to check for key existence. * * It stops at top parent if no vm in the chain has the * key. It then creates any missing bindings on the * final resolved vm. */ function traceScope (path, compiler, data) { var rel = '', dist = 0, self = compiler if (data && utils.get(data, path) !== undefined) { // hack: temporarily attached data return '$temp.' } while (compiler) { if (compiler.hasKey(path)) { break } else { compiler = compiler.parent dist++ } } if (compiler) { while (dist--) { rel += '$parent.' } if (!compiler.bindings[path] && path.charAt(0) !== '$') { compiler.createBinding(path) } } else { self.createBinding(path) } return rel } /** * Create a function from a string... * this looks like evil magic but since all variables are limited * to the VM's data it's actually properly sandboxed */ function makeGetter (exp, raw) { var fn try { fn = new Function(exp) } catch (e) { utils.warn('Error parsing expression: ' + raw) } return fn } /** * Escape a leading dollar sign for regex construction */ function escapeDollar (v) { return v.charAt(0) === '$' ? '\\' + v : v } /** * Parse and return an anonymous computed property getter function * from an arbitrary expression, together with a list of paths to be * created as bindings. */ exports.parse = function (exp, compiler, data) { // unicode and 'constructor' are not allowed for XSS security. if (UNICODE_RE.test(exp) || CTOR_RE.test(exp)) { utils.warn('Unsafe expression: ' + exp) return } // extract variable names var vars = getVariables(exp) if (!vars.length) { return makeGetter('return ' + exp, exp) } vars = utils.unique(vars) var accessors = '', has = utils.hash(), strings = [], // construct a regex to extract all valid variable paths // ones that begin with "$" are particularly tricky // because we can't use \b for them pathRE = new RegExp( "[^$\\w\\.](" + vars.map(escapeDollar).join('|') + ")[$\\w\\.]*\\b", 'g' ), body = (' ' + exp) .replace(STR_SAVE_RE, saveStrings) .replace(pathRE, replacePath) .replace(STR_RESTORE_RE, restoreStrings) body = accessors + 'return ' + body function saveStrings (str) { var i = strings.length // escape newlines in strings so the expression // can be correctly evaluated strings[i] = str.replace(NEWLINE_RE, '\\n') return '"' + i + '"' } function replacePath (path) { // keep track of the first char var c = path.charAt(0) path = path.slice(1) var val = 'this.' + traceScope(path, compiler, data) + path if (!has[path]) { accessors += val + ';' has[path] = 1 } // don't forget to put that first char back return c + val } function restoreStrings (str, i) { return strings[i] } return makeGetter(body, exp) } /** * Evaluate an expression in the context of a compiler. * Accepts additional data. */ exports.eval = function (exp, compiler, data) { var getter = exports.parse(exp, compiler, data), res if (getter) { // hack: temporarily attach the additional data so // it can be accessed in the getter compiler.vm.$temp = data res = getter.call(compiler.vm) delete compiler.vm.$temp } return res }