UNPKG

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