UNPKG

silk-gui

Version:

GUI for developers and Node OS

297 lines (256 loc) 5.65 kB
var _ = require('../util') var Cache = require('../cache') var pathCache = new Cache(1000) var identRE = /^[$_a-zA-Z]+[\w$]*$/ /** * Path-parsing algorithm scooped from Polymer/observe-js */ 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 () {} /** * Determine the type of a character in a keypath. * * @param {Char} char * @return {String} type */ 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' } /** * Parse a string path into an array of segments * Todo implement cache * * @param {String} path * @return {Array|undefined} */ function parsePath (path) { var keys = [] var index = -1 var mode = 'beforePath' var c, newChar, key, type, transition, action, typeMap 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 () { 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()) { 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 } } } /** * Format a accessor segment based on its type. * * @param {String} key * @return {Boolean} */ function formatAccessor(key) { if (identRE.test(key)) { // identifier return '.' + key } else if (+key === key >>> 0) { // bracket index return '[' + key + ']' } else { // bracket string return '["' + key.replace(/"/g, '\\"') + '"]' } } /** * Compiles a getter function with a fixed path. * * @param {Array} path * @return {Function} */ exports.compileGetter = function (path) { var body = 'return o' + path.map(formatAccessor).join('') return new Function('o', body) } /** * External parse that check for a cache hit first * * @param {String} path * @return {Array|undefined} */ exports.parse = function (path) { var hit = pathCache.get(path) if (!hit) { hit = parsePath(path) if (hit) { hit.get = exports.compileGetter(hit) pathCache.put(path, hit) } } return hit } /** * Get from an object from a path string * * @param {Object} obj * @param {String} path */ exports.get = function (obj, path) { path = exports.parse(path) if (path) { return path.get(obj) } } /** * Set on an object from a path * * @param {Object} obj * @param {String | Array} path * @param {*} val */ exports.set = function (obj, path, val) { if (typeof path === 'string') { path = exports.parse(path) } if (!path || !_.isObject(obj)) { return false } var last, key for (var i = 0, l = path.length - 1; i < l; i++) { last = obj key = path[i] obj = obj[key] if (!_.isObject(obj)) { obj = {} last.$add(key, obj) } } key = path[i] if (key in obj) { obj[key] = val } else { obj.$add(key, val) } return true }