path.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. var Cache = require('../cache')
  2. var pathCache = new Cache(1000)
  3. var identRE = /^[$_a-zA-Z]+[\w$]*$/
  4. /**
  5. * Path-parsing algorithm scooped from Polymer/observe-js
  6. */
  7. var pathStateMachine = {
  8. 'beforePath': {
  9. 'ws': ['beforePath'],
  10. 'ident': ['inIdent', 'append'],
  11. '[': ['beforeElement'],
  12. 'eof': ['afterPath']
  13. },
  14. 'inPath': {
  15. 'ws': ['inPath'],
  16. '.': ['beforeIdent'],
  17. '[': ['beforeElement'],
  18. 'eof': ['afterPath']
  19. },
  20. 'beforeIdent': {
  21. 'ws': ['beforeIdent'],
  22. 'ident': ['inIdent', 'append']
  23. },
  24. 'inIdent': {
  25. 'ident': ['inIdent', 'append'],
  26. '0': ['inIdent', 'append'],
  27. 'number': ['inIdent', 'append'],
  28. 'ws': ['inPath', 'push'],
  29. '.': ['beforeIdent', 'push'],
  30. '[': ['beforeElement', 'push'],
  31. 'eof': ['afterPath', 'push']
  32. },
  33. 'beforeElement': {
  34. 'ws': ['beforeElement'],
  35. '0': ['afterZero', 'append'],
  36. 'number': ['inIndex', 'append'],
  37. "'": ['inSingleQuote', 'append', ''],
  38. '"': ['inDoubleQuote', 'append', '']
  39. },
  40. 'afterZero': {
  41. 'ws': ['afterElement', 'push'],
  42. ']': ['inPath', 'push']
  43. },
  44. 'inIndex': {
  45. '0': ['inIndex', 'append'],
  46. 'number': ['inIndex', 'append'],
  47. 'ws': ['afterElement'],
  48. ']': ['inPath', 'push']
  49. },
  50. 'inSingleQuote': {
  51. "'": ['afterElement'],
  52. 'eof': 'error',
  53. 'else': ['inSingleQuote', 'append']
  54. },
  55. 'inDoubleQuote': {
  56. '"': ['afterElement'],
  57. 'eof': 'error',
  58. 'else': ['inDoubleQuote', 'append']
  59. },
  60. 'afterElement': {
  61. 'ws': ['afterElement'],
  62. ']': ['inPath', 'push']
  63. }
  64. }
  65. function noop () {}
  66. /**
  67. * Determine the type of a character in a keypath.
  68. *
  69. * @param {Char} char
  70. * @return {String} type
  71. */
  72. function getPathCharType (char) {
  73. if (char === undefined) {
  74. return 'eof'
  75. }
  76. var code = char.charCodeAt(0)
  77. switch(code) {
  78. case 0x5B: // [
  79. case 0x5D: // ]
  80. case 0x2E: // .
  81. case 0x22: // "
  82. case 0x27: // '
  83. case 0x30: // 0
  84. return char
  85. case 0x5F: // _
  86. case 0x24: // $
  87. return 'ident'
  88. case 0x20: // Space
  89. case 0x09: // Tab
  90. case 0x0A: // Newline
  91. case 0x0D: // Return
  92. case 0xA0: // No-break space
  93. case 0xFEFF: // Byte Order Mark
  94. case 0x2028: // Line Separator
  95. case 0x2029: // Paragraph Separator
  96. return 'ws'
  97. }
  98. // a-z, A-Z
  99. if ((0x61 <= code && code <= 0x7A) ||
  100. (0x41 <= code && code <= 0x5A)) {
  101. return 'ident'
  102. }
  103. // 1-9
  104. if (0x31 <= code && code <= 0x39) {
  105. return 'number'
  106. }
  107. return 'else'
  108. }
  109. /**
  110. * Parse a string path into an array of segments
  111. * Todo implement cache
  112. *
  113. * @param {String} path
  114. * @return {Array|undefined}
  115. */
  116. function parsePath (path) {
  117. var keys = []
  118. var index = -1
  119. var mode = 'beforePath'
  120. var c, newChar, key, type, transition, action, typeMap
  121. var actions = {
  122. push: function() {
  123. if (key === undefined) {
  124. return
  125. }
  126. keys.push(key)
  127. key = undefined
  128. },
  129. append: function() {
  130. if (key === undefined) {
  131. key = newChar
  132. } else {
  133. key += newChar
  134. }
  135. }
  136. }
  137. function maybeUnescapeQuote () {
  138. var nextChar = path[index + 1]
  139. if ((mode === 'inSingleQuote' && nextChar === "'") ||
  140. (mode === 'inDoubleQuote' && nextChar === '"')) {
  141. index++
  142. newChar = nextChar
  143. actions.append()
  144. return true
  145. }
  146. }
  147. while (mode) {
  148. index++
  149. c = path[index]
  150. if (c === '\\' && maybeUnescapeQuote()) {
  151. continue
  152. }
  153. type = getPathCharType(c)
  154. typeMap = pathStateMachine[mode]
  155. transition = typeMap[type] || typeMap['else'] || 'error'
  156. if (transition === 'error') {
  157. return // parse error
  158. }
  159. mode = transition[0]
  160. action = actions[transition[1]] || noop
  161. newChar = transition[2] === undefined
  162. ? c
  163. : transition[2]
  164. action()
  165. if (mode === 'afterPath') {
  166. return keys
  167. }
  168. }
  169. }
  170. /**
  171. * Format a accessor segment based on its type.
  172. *
  173. * @param {String} key
  174. * @return {Boolean}
  175. */
  176. function formatAccessor(key) {
  177. if (identRE.test(key)) { // identifier
  178. return '.' + key
  179. } else if (+key === key >>> 0) { // bracket index
  180. return '[' + key + ']';
  181. } else { // bracket string
  182. return '["' + key.replace(/"/g, '\\"') + '"]';
  183. }
  184. }
  185. /**
  186. * Compiles a getter function with a fixed path.
  187. *
  188. * @param {Array} path
  189. * @return {Function}
  190. */
  191. exports.compileGetter = function (path) {
  192. var body =
  193. 'try{return o' +
  194. path.map(formatAccessor).join('') +
  195. '}catch(e){};'
  196. return new Function('o', body)
  197. }
  198. /**
  199. * External parse that check for a cache hit first
  200. *
  201. * @param {String} path
  202. * @return {Array|undefined}
  203. */
  204. exports.parse = function (path) {
  205. var hit = pathCache.get(path)
  206. if (!hit) {
  207. hit = parsePath(path)
  208. if (hit) {
  209. hit.get = exports.compileGetter(hit)
  210. pathCache.put(path, hit)
  211. }
  212. }
  213. return hit
  214. }
  215. /**
  216. * Get from an object from a path string
  217. *
  218. * @param {Object} obj
  219. * @param {String} path
  220. */
  221. exports.get = function (obj, path) {
  222. path = exports.parse(path)
  223. if (path) {
  224. return path.get(obj)
  225. }
  226. }
  227. /**
  228. * Set on an object from a path
  229. *
  230. * @param {Object} obj
  231. * @param {String | Array} path
  232. * @param {*} val
  233. */
  234. exports.set = function (obj, path, val) {
  235. if (typeof path === 'string') {
  236. path = exports.parse(path)
  237. }
  238. if (!path || !isSettable(obj)) {
  239. return false
  240. }
  241. var last, key
  242. for (var i = 0, l = path.length - 1; i < l; i++) {
  243. last = obj
  244. key = path[i]
  245. obj = obj[key]
  246. if (!isSettable(obj)) {
  247. obj = {}
  248. add(last, key, obj)
  249. }
  250. }
  251. key = path[i]
  252. if (obj.hasOwnProperty(key)) {
  253. obj[key] = val
  254. } else {
  255. add(obj, key, val)
  256. }
  257. return true
  258. }
  259. /**
  260. * Check if a value is an object that can have values
  261. * set on it. Slightly faster than _.isObject.
  262. *
  263. * @param {*} val
  264. * @return {Boolean}
  265. */
  266. function isSettable (val) {
  267. return val && typeof val === 'object'
  268. }
  269. /**
  270. * Add a property to an object, using $add if target
  271. * has been augmented by Vue's observer.
  272. *
  273. * @param {Object} obj
  274. * @param {String} key
  275. * @param {*} val
  276. */
  277. function add (obj, key, val) {
  278. if (obj.$add) {
  279. obj.$add(key, val)
  280. } else {
  281. obj[key] = val
  282. }
  283. }