UNPKG

silk-gui

Version:

GUI for developers and Node OS

255 lines (231 loc) 6.04 kB
var _ = require('../util') var Path = require('./path') var Cache = require('../cache') var expressionCache = new Cache(1000) var allowedKeywords = 'Math,Date,this,true,false,null,undefined,Infinity,NaN,' + 'isNaN,isFinite,decodeURI,decodeURIComponent,encodeURI,' + 'encodeURIComponent,parseInt,parseFloat' var allowedKeywordsRE = new RegExp('^(' + allowedKeywords.replace(/,/g, '\\b|') + '\\b)') // keywords that don't make sense inside expressions var improperKeywords = 'break,case,class,catch,const,continue,debugger,default,' + 'delete,do,else,export,extends,finally,for,function,if,' + 'import,in,instanceof,let,return,super,switch,throw,try,' + 'var,while,with,yield,enum,await,implements,package,' + 'proctected,static,interface,private,public' var improperKeywordsRE = new RegExp('^(' + improperKeywords.replace(/,/g, '\\b|') + '\\b)') var wsRE = /\s/g var newlineRE = /\n/g var saveRE = /[\{,]\s*[\w\$_]+\s*:|('[^']*'|"[^"]*")|new |typeof |void /g var restoreRE = /"(\d+)"/g var pathTestRE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\]|\[\d+\])*$/ var pathReplaceRE = /[^\w$\.]([A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\])*)/g var booleanLiteralRE = /^(true|false)$/ /** * Save / Rewrite / Restore * * When rewriting paths found in an expression, it is * possible for the same letter sequences to be found in * strings and Object literal property keys. Therefore we * remove and store these parts in a temporary array, and * restore them after the path rewrite. */ var saved = [] /** * Save replacer * * The save regex can match two possible cases: * 1. An opening object literal * 2. A string * If matched as a plain string, we need to escape its * newlines, since the string needs to be preserved when * generating the function body. * * @param {String} str * @param {String} isString - str if matched as a string * @return {String} - placeholder with index */ function save (str, isString) { var i = saved.length saved[i] = isString ? str.replace(newlineRE, '\\n') : str return '"' + i + '"' } /** * Path rewrite replacer * * @param {String} raw * @return {String} */ function rewrite (raw) { var c = raw.charAt(0) var path = raw.slice(1) if (allowedKeywordsRE.test(path)) { return raw } else { path = path.indexOf('"') > -1 ? path.replace(restoreRE, restore) : path return c + 'scope.' + path } } /** * Restore replacer * * @param {String} str * @param {String} i - matched save index * @return {String} */ function restore (str, i) { return saved[i] } /** * Rewrite an expression, prefixing all path accessors with * `scope.` and generate getter/setter functions. * * @param {String} exp * @param {Boolean} needSet * @return {Function} */ function compileExpFns (exp, needSet) { if (improperKeywordsRE.test(exp)) { _.warn( 'Avoid using reserved keywords in expression: ' + exp ) } // reset state saved.length = 0 // save strings and object literal keys var body = exp .replace(saveRE, save) .replace(wsRE, '') // rewrite all paths // pad 1 space here becaue the regex matches 1 extra char body = (' ' + body) .replace(pathReplaceRE, rewrite) .replace(restoreRE, restore) var getter = makeGetter(body) if (getter) { return { get: getter, body: body, set: needSet ? makeSetter(body) : null } } } /** * Compile getter setters for a simple path. * * @param {String} exp * @return {Function} */ function compilePathFns (exp) { var getter, path if (exp.indexOf('[') < 0) { // really simple path path = exp.split('.') getter = Path.compileGetter(path) } else { // do the real parsing path = Path.parse(exp) getter = path.get } return { get: getter, // always generate setter for simple paths set: function (obj, val) { Path.set(obj, path, val) } } } /** * Build a getter function. Requires eval. * * We isolate the try/catch so it doesn't affect the * optimization of the parse function when it is not called. * * @param {String} body * @return {Function|undefined} */ function makeGetter (body) { try { return new Function('scope', 'return ' + body + ';') } catch (e) { _.warn( 'Invalid expression. ' + 'Generated function body: ' + body ) } } /** * Build a setter function. * * This is only needed in rare situations like "a[b]" where * a settable path requires dynamic evaluation. * * This setter function may throw error when called if the * expression body is not a valid left-hand expression in * assignment. * * @param {String} body * @return {Function|undefined} */ function makeSetter (body) { try { return new Function('scope', 'value', body + '=value;') } catch (e) { _.warn('Invalid setter function body: ' + body) } } /** * Check for setter existence on a cache hit. * * @param {Function} hit */ function checkSetter (hit) { if (!hit.set) { hit.set = makeSetter(hit.body) } } /** * Parse an expression into re-written getter/setters. * * @param {String} exp * @param {Boolean} needSet * @return {Function} */ exports.parse = function (exp, needSet) { exp = exp.trim() // try cache var hit = expressionCache.get(exp) if (hit) { if (needSet) { checkSetter(hit) } return hit } // we do a simple path check to optimize for them. // the check fails valid paths with unusal whitespaces, // but that's too rare and we don't care. // also skip boolean literals and paths that start with // global "Math" var res = pathTestRE.test(exp) && // don't treat true/false as paths !booleanLiteralRE.test(exp) && // Math constants e.g. Math.PI, Math.E etc. exp.slice(0, 5) !== 'Math.' ? compilePathFns(exp) : compileExpFns(exp, needSet) expressionCache.put(exp, res) return res } // Export the pathRegex for external use exports.pathTestRE = pathTestRE