/* * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt * Code distributed by Google as part of the polymer project is also * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt */ 'use strict'; function detectEval() { // Don't test for eval if we're running in a Chrome App environment. // We check for APIs set that only exist in a Chrome App context. if (typeof chrome !== 'undefined' && chrome.app && chrome.app.runtime) { return false; } // Firefox OS Apps do not allow eval. This feature detection is very hacky // but even if some other platform adds support for this function this code // will continue to work. if (typeof navigator != 'undefined' && navigator.getDeviceStorage) { return false; } try { var f = new Function('', 'return true;'); return f(); } catch (ex) { return false; } } var hasEval = detectEval(); function isIndex(s) { return +s === s >>> 0 && s !== ''; } function toNumber(s) { return +s; } function isObject(obj) { return obj === Object(obj); } var numberIsNaN = Number.isNaN || function (value) { return typeof value === 'number' && isNaN(value); } function areSameValue(left, right) { if (left === right) return left !== 0 || 1 / left === 1 / right; if (numberIsNaN(left) && numberIsNaN(right)) return true; return left !== left && right !== right; } var createObject = ('__proto__' in {}) ? function (obj) { return obj; } : function (obj) { var proto = obj.__proto__; if (!proto) return obj; var newObject = Object.create(proto); Object.getOwnPropertyNames(obj).forEach(function (name) { Object.defineProperty(newObject, name, Object.getOwnPropertyDescriptor(obj, name)); }); return newObject; }; var identStart = '[\$_a-zA-Z]'; var identPart = '[\$_a-zA-Z0-9]'; var identRegExp = new RegExp('^' + identStart + '+' + identPart + '*' + '$'); function getPathCharType(char) { if (char === undefined) return 'eof'; var code = char.charCodeAt(0); switch (code) { case 0x5B: // [ case 0x5D: // ] case 0x2E: // . case 0x22: // " case 0x27: // ' case 0x30: // 0 return char; case 0x5F: // _ case 0x24: // $ return 'ident'; case 0x20: // Space case 0x09: // Tab case 0x0A: // Newline case 0x0D: // Return case 0xA0: // No-break space case 0xFEFF: // Byte Order Mark case 0x2028: // Line Separator case 0x2029: // Paragraph Separator return 'ws'; } // a-z, A-Z if ((0x61 <= code && code <= 0x7A) || (0x41 <= code && code <= 0x5A)) return 'ident'; // 1-9 if (0x31 <= code && code <= 0x39) return 'number'; return 'else'; } var pathStateMachine = { 'beforePath': { 'ws': ['beforePath'], 'ident': ['inIdent', 'append'], '[': ['beforeElement'], 'eof': ['afterPath'] }, 'inPath': { 'ws': ['inPath'], '.': ['beforeIdent'], '[': ['beforeElement'], 'eof': ['afterPath'] }, 'beforeIdent': { 'ws': ['beforeIdent'], 'ident': ['inIdent', 'append'] }, 'inIdent': { 'ident': ['inIdent', 'append'], '0': ['inIdent', 'append'], 'number': ['inIdent', 'append'], 'ws': ['inPath', 'push'], '.': ['beforeIdent', 'push'], '[': ['beforeElement', 'push'], 'eof': ['afterPath', 'push'] }, 'beforeElement': { 'ws': ['beforeElement'], '0': ['afterZero', 'append'], 'number': ['inIndex', 'append'], "'": ['inSingleQuote', 'append', ''], '"': ['inDoubleQuote', 'append', ''] }, 'afterZero': { 'ws': ['afterElement', 'push'], ']': ['inPath', 'push'] }, 'inIndex': { '0': ['inIndex', 'append'], 'number': ['inIndex', 'append'], 'ws': ['afterElement'], ']': ['inPath', 'push'] }, 'inSingleQuote': { "'": ['afterElement'], 'eof': ['error'], 'else': ['inSingleQuote', 'append'] }, 'inDoubleQuote': { '"': ['afterElement'], 'eof': ['error'], 'else': ['inDoubleQuote', 'append'] }, 'afterElement': { 'ws': ['afterElement'], ']': ['inPath', 'push'] } } function noop() { } function parsePath(path) { var keys = []; var index = -1; var c, newChar, key, type, transition, action, typeMap, mode = 'beforePath'; var actions = { push: function () { if (key === undefined) return; keys.push(key); key = undefined; }, append: function () { if (key === undefined) key = newChar else key += newChar; } }; function maybeUnescapeQuote() { if (index >= path.length) return; var nextChar = path[index + 1]; if ((mode == 'inSingleQuote' && nextChar == "'") || (mode == 'inDoubleQuote' && nextChar == '"')) { index++; newChar = nextChar; actions.append(); return true; } } while (mode) { index++; c = path[index]; if (c == '\\' && maybeUnescapeQuote(mode)) continue; type = getPathCharType(c); typeMap = pathStateMachine[mode]; transition = typeMap[type] || typeMap['else'] || 'error'; if (transition == 'error') return; // parse error; mode = transition[0]; action = actions[transition[1]] || noop; newChar = transition[2] === undefined ? c : transition[2]; action(); if (mode === 'afterPath') { return keys; } } return; // parse error } function isIdent(s) { return identRegExp.test(s); } var constructorIsPrivate = {}; function Path(parts, privateToken) { if (privateToken !== constructorIsPrivate) throw Error('Use Path.get to retrieve path objects'); for (var i = 0; i < parts.length; i++) { this.push(String(parts[i])); } if (hasEval && this.length) { this.getValueFrom = this.compiledGetValueFromFn(); } } // TODO(rafaelw): Make simple LRU cache var pathCache = {}; function getPath(pathString) { if (pathString instanceof Path) return pathString; if (pathString == null || pathString.length == 0) pathString = ''; if (typeof pathString != 'string') { if (isIndex(pathString.length)) { // Constructed with array-like (pre-parsed) keys return new Path(pathString, constructorIsPrivate); } pathString = String(pathString); } var path = pathCache[pathString]; if (path) return path; var parts = parsePath(pathString); if (!parts) return invalidPath; var path = new Path(parts, constructorIsPrivate); pathCache[pathString] = path; return path; } Path.get = getPath; function formatAccessor(key) { if (isIndex(key)) { return '[' + key + ']'; } else { return '["' + key.replace(/"/g, '\\"') + '"]'; } } Path.prototype = createObject({ __proto__: [], valid: true, toString: function () { var pathString = ''; for (var i = 0; i < this.length; i++) { var key = this[i]; if (isIdent(key)) { pathString += i ? '.' + key : key; } else { pathString += formatAccessor(key); } } return pathString; }, getValueFrom: function (obj, directObserver) { for (var i = 0; i < this.length; i++) { if (obj == null) return; obj = obj[this[i]]; } return obj; }, iterateObjects: function (obj, observe) { for (var i = 0; i < this.length; i++) { if (i) obj = obj[this[i - 1]]; if (!isObject(obj)) return; observe(obj, this[i]); } }, compiledGetValueFromFn: function () { var str = ''; var pathString = 'obj'; str += 'if (obj != null'; var i = 0; var key; for (; i < (this.length - 1) ; i++) { key = this[i]; pathString += isIdent(key) ? '.' + key : formatAccessor(key); str += ' &&\n ' + pathString + ' != null'; } str += ')\n'; var key = this[i]; pathString += isIdent(key) ? '.' + key : formatAccessor(key); str += ' return ' + pathString + ';\nelse\n return undefined;'; return new Function('obj', str); }, setValueFrom: function (obj, value) { if (!this.length) return false; for (var i = 0; i < this.length - 1; i++) { if (!isObject(obj)) return false; obj = obj[this[i]]; } if (!isObject(obj)) return false; obj[this[i]] = value; return true; } }); var invalidPath = new Path('', constructorIsPrivate); invalidPath.valid = false; invalidPath.getValueFrom = invalidPath.setValueFrom = function () { }; exports.Path = Path;