path.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. /* @flow */
  2. import { isObject } from './util'
  3. /**
  4. * Path parser
  5. * - Inspired:
  6. * Vue.js Path parser
  7. */
  8. // actions
  9. const APPEND = 0
  10. const PUSH = 1
  11. const INC_SUB_PATH_DEPTH = 2
  12. const PUSH_SUB_PATH = 3
  13. // states
  14. const BEFORE_PATH = 0
  15. const IN_PATH = 1
  16. const BEFORE_IDENT = 2
  17. const IN_IDENT = 3
  18. const IN_SUB_PATH = 4
  19. const IN_SINGLE_QUOTE = 5
  20. const IN_DOUBLE_QUOTE = 6
  21. const AFTER_PATH = 7
  22. const ERROR = 8
  23. const pathStateMachine: any = []
  24. pathStateMachine[BEFORE_PATH] = {
  25. 'ws': [BEFORE_PATH],
  26. 'ident': [IN_IDENT, APPEND],
  27. '[': [IN_SUB_PATH],
  28. 'eof': [AFTER_PATH]
  29. }
  30. pathStateMachine[IN_PATH] = {
  31. 'ws': [IN_PATH],
  32. '.': [BEFORE_IDENT],
  33. '[': [IN_SUB_PATH],
  34. 'eof': [AFTER_PATH]
  35. }
  36. pathStateMachine[BEFORE_IDENT] = {
  37. 'ws': [BEFORE_IDENT],
  38. 'ident': [IN_IDENT, APPEND],
  39. '0': [IN_IDENT, APPEND],
  40. 'number': [IN_IDENT, APPEND]
  41. }
  42. pathStateMachine[IN_IDENT] = {
  43. 'ident': [IN_IDENT, APPEND],
  44. '0': [IN_IDENT, APPEND],
  45. 'number': [IN_IDENT, APPEND],
  46. 'ws': [IN_PATH, PUSH],
  47. '.': [BEFORE_IDENT, PUSH],
  48. '[': [IN_SUB_PATH, PUSH],
  49. 'eof': [AFTER_PATH, PUSH]
  50. }
  51. pathStateMachine[IN_SUB_PATH] = {
  52. "'": [IN_SINGLE_QUOTE, APPEND],
  53. '"': [IN_DOUBLE_QUOTE, APPEND],
  54. '[': [IN_SUB_PATH, INC_SUB_PATH_DEPTH],
  55. ']': [IN_PATH, PUSH_SUB_PATH],
  56. 'eof': ERROR,
  57. 'else': [IN_SUB_PATH, APPEND]
  58. }
  59. pathStateMachine[IN_SINGLE_QUOTE] = {
  60. "'": [IN_SUB_PATH, APPEND],
  61. 'eof': ERROR,
  62. 'else': [IN_SINGLE_QUOTE, APPEND]
  63. }
  64. pathStateMachine[IN_DOUBLE_QUOTE] = {
  65. '"': [IN_SUB_PATH, APPEND],
  66. 'eof': ERROR,
  67. 'else': [IN_DOUBLE_QUOTE, APPEND]
  68. }
  69. /**
  70. * Check if an expression is a literal value.
  71. */
  72. const literalValueRE: RegExp = /^\s?(?:true|false|-?[\d.]+|'[^']*'|"[^"]*")\s?$/
  73. function isLiteral (exp: string): boolean {
  74. return literalValueRE.test(exp)
  75. }
  76. /**
  77. * Strip quotes from a string
  78. */
  79. function stripQuotes (str: string): string | boolean {
  80. const a: number = str.charCodeAt(0)
  81. const b: number = str.charCodeAt(str.length - 1)
  82. return a === b && (a === 0x22 || a === 0x27)
  83. ? str.slice(1, -1)
  84. : str
  85. }
  86. /**
  87. * Determine the type of a character in a keypath.
  88. */
  89. function getPathCharType (ch: ?string): string {
  90. if (ch === undefined || ch === null) { return 'eof' }
  91. const code: number = ch.charCodeAt(0)
  92. switch (code) {
  93. case 0x5B: // [
  94. case 0x5D: // ]
  95. case 0x2E: // .
  96. case 0x22: // "
  97. case 0x27: // '
  98. return ch
  99. case 0x5F: // _
  100. case 0x24: // $
  101. case 0x2D: // -
  102. return 'ident'
  103. case 0x09: // Tab
  104. case 0x0A: // Newline
  105. case 0x0D: // Return
  106. case 0xA0: // No-break space
  107. case 0xFEFF: // Byte Order Mark
  108. case 0x2028: // Line Separator
  109. case 0x2029: // Paragraph Separator
  110. return 'ws'
  111. }
  112. return 'ident'
  113. }
  114. /**
  115. * Format a subPath, return its plain form if it is
  116. * a literal string or number. Otherwise prepend the
  117. * dynamic indicator (*).
  118. */
  119. function formatSubPath (path: string): boolean | string {
  120. const trimmed: string = path.trim()
  121. // invalid leading 0
  122. if (path.charAt(0) === '0' && isNaN(path)) { return false }
  123. return isLiteral(trimmed) ? stripQuotes(trimmed) : '*' + trimmed
  124. }
  125. /**
  126. * Parse a string path into an array of segments
  127. */
  128. function parse (path: Path): ?Array<string> {
  129. const keys: Array<string> = []
  130. let index: number = -1
  131. let mode: number = BEFORE_PATH
  132. let subPathDepth: number = 0
  133. let c: ?string
  134. let key: any
  135. let newChar: any
  136. let type: string
  137. let transition: number
  138. let action: Function
  139. let typeMap: any
  140. const actions: Array<Function> = []
  141. actions[PUSH] = function () {
  142. if (key !== undefined) {
  143. keys.push(key)
  144. key = undefined
  145. }
  146. }
  147. actions[APPEND] = function () {
  148. if (key === undefined) {
  149. key = newChar
  150. } else {
  151. key += newChar
  152. }
  153. }
  154. actions[INC_SUB_PATH_DEPTH] = function () {
  155. actions[APPEND]()
  156. subPathDepth++
  157. }
  158. actions[PUSH_SUB_PATH] = function () {
  159. if (subPathDepth > 0) {
  160. subPathDepth--
  161. mode = IN_SUB_PATH
  162. actions[APPEND]()
  163. } else {
  164. subPathDepth = 0
  165. if (key === undefined) { return false }
  166. key = formatSubPath(key)
  167. if (key === false) {
  168. return false
  169. } else {
  170. actions[PUSH]()
  171. }
  172. }
  173. }
  174. function maybeUnescapeQuote (): ?boolean {
  175. const nextChar: string = path[index + 1]
  176. if ((mode === IN_SINGLE_QUOTE && nextChar === "'") ||
  177. (mode === IN_DOUBLE_QUOTE && nextChar === '"')) {
  178. index++
  179. newChar = '\\' + nextChar
  180. actions[APPEND]()
  181. return true
  182. }
  183. }
  184. while (mode !== null) {
  185. index++
  186. c = path[index]
  187. if (c === '\\' && maybeUnescapeQuote()) {
  188. continue
  189. }
  190. type = getPathCharType(c)
  191. typeMap = pathStateMachine[mode]
  192. transition = typeMap[type] || typeMap['else'] || ERROR
  193. if (transition === ERROR) {
  194. return // parse error
  195. }
  196. mode = transition[0]
  197. action = actions[transition[1]]
  198. if (action) {
  199. newChar = transition[2]
  200. newChar = newChar === undefined
  201. ? c
  202. : newChar
  203. if (action() === false) {
  204. return
  205. }
  206. }
  207. if (mode === AFTER_PATH) {
  208. return keys
  209. }
  210. }
  211. }
  212. export type PathValue = PathValueObject | PathValueArray | Function | string | number | boolean | null
  213. export type PathValueObject = { [key: string]: PathValue }
  214. export type PathValueArray = Array<PathValue>
  215. export default class I18nPath {
  216. _cache: Object
  217. constructor () {
  218. this._cache = Object.create(null)
  219. }
  220. /**
  221. * External parse that check for a cache hit first
  222. */
  223. parsePath (path: Path): Array<string> {
  224. let hit: ?Array<string> = this._cache[path]
  225. if (!hit) {
  226. hit = parse(path)
  227. if (hit) {
  228. this._cache[path] = hit
  229. }
  230. }
  231. return hit || []
  232. }
  233. /**
  234. * Get path value from path string
  235. */
  236. getPathValue (obj: mixed, path: Path): PathValue {
  237. if (!isObject(obj)) { return null }
  238. const paths: Array<string> = this.parsePath(path)
  239. if (paths.length === 0) {
  240. return null
  241. } else {
  242. const length: number = paths.length
  243. let last: any = obj
  244. let i: number = 0
  245. while (i < length) {
  246. const value: any = last[paths[i]]
  247. if (value === undefined || value === null) {
  248. return null
  249. }
  250. last = value
  251. i++
  252. }
  253. return last
  254. }
  255. }
  256. }