path.js 5.8 KB

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