silk-gui
Version:
GUI for developers and Node OS
255 lines (231 loc) • 6.04 kB
JavaScript
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