/** * Based on Kendo UI Core expression code */ 'use strict' function Cache(maxSize) { this._maxSize = maxSize this.clear() } Cache.prototype.clear = function() { this._size = 0 this._values = {} } Cache.prototype.get = function(key) { return this._values[key] } Cache.prototype.set = function(key, value) { this._size >= this._maxSize && this.clear() if (!this._values.hasOwnProperty(key)) { this._size++ } return this._values[key] = value } var SPLIT_REGEX = /[^.^\]^[]+|(?=\[\]|\.\.)/g, DIGIT_REGEX = /^\d+$/, LEAD_DIGIT_REGEX = /^\d/, SPEC_CHAR_REGEX = /[~`!#$%\^&*+=\-\[\]\\';,/{}|\\":<>\?]/g, CLEAN_QUOTES_REGEX = /^\s*(['"]?)(.*?)(\1)\s*$/, MAX_CACHE_SIZE = 512 var contentSecurityPolicy = false, pathCache = new Cache(MAX_CACHE_SIZE), setCache = new Cache(MAX_CACHE_SIZE), getCache = new Cache(MAX_CACHE_SIZE) try { new Function('') } catch (error) { contentSecurityPolicy = true } module.exports = { Cache: Cache, expr: expr, split: split, normalizePath: normalizePath, setter: contentSecurityPolicy ? function(path) { var parts = normalizePath(path) return function(data, value) { return setterFallback(parts, data, value) } } : function(path) { return setCache.get(path) || setCache.set( path, new Function( 'data, value', expr(path, 'data') + ' = value' ) ) }, getter: contentSecurityPolicy ? function(path, safe) { var parts = normalizePath(path) return function(data) { return getterFallback(parts, safe, data) } } : function(path, safe) { var key = path + '_' + safe return getCache.get(key) || getCache.set( key, new Function('data', 'return ' + expr(path, safe, 'data')) ) }, join: function(segments) { return segments.reduce(function(path, part) { return ( path + (isQuoted(part) || DIGIT_REGEX.test(part) ? '[' + part + ']' : (path ? '.' : '') + part) ) }, '') }, forEach: function(path, cb, thisArg) { forEach(split(path), cb, thisArg) } } function setterFallback(parts, data, value) { var index = 0, len = parts.length while (index < len - 1) { data = data[parts[index++]] } data[parts[index]] = value } function getterFallback(parts, safe, data) { var index = 0, len = parts.length while (index < len) { if (data != null || !safe) { data = data[parts[index++]] } else { return } } return data } function normalizePath(path) { return pathCache.get(path) || pathCache.set( path, split(path).map(function(part) { return part.replace(CLEAN_QUOTES_REGEX, '$2') }) ) } function split(path) { return path.match(SPLIT_REGEX) } function expr(expression, safe, param) { expression = expression || '' if (typeof safe === 'string') { param = safe safe = false } param = param || 'data' if (expression && expression.charAt(0) !== '[') expression = '.' + expression return safe ? makeSafe(expression, param) : param + expression } function forEach(parts, iter, thisArg) { var len = parts.length, part, idx, isArray, isBracket for (idx = 0; idx < len; idx++) { part = parts[idx] if (part) { if (shouldBeQuoted(part)) { part = '"' + part + '"' } isBracket = isQuoted(part) isArray = !isBracket && /^\d+$/.test(part) iter.call(thisArg, part, isBracket, isArray, idx, parts) } } } function isQuoted(str) { return ( typeof str === 'string' && str && ["'", '"'].indexOf(str.charAt(0)) !== -1 ) } function makeSafe(path, param) { var result = param, parts = split(path), isLast forEach(parts, function(part, isBracket, isArray, idx, parts) { isLast = idx === parts.length - 1 part = isBracket || isArray ? '[' + part + ']' : '.' + part result += part + (!isLast ? ' || {})' : ')') }) return new Array(parts.length + 1).join('(') + result } function hasLeadingNumber(part) { return part.match(LEAD_DIGIT_REGEX) && !part.match(DIGIT_REGEX) } function hasSpecialChars(part) { return SPEC_CHAR_REGEX.test(part) } function shouldBeQuoted(part) { return !isQuoted(part) && (hasLeadingNumber(part) || hasSpecialChars(part)) }