1 |
|
2 |
|
3 | import { isObject } from './util'
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | const APPEND = 0
|
13 | const PUSH = 1
|
14 | const INC_SUB_PATH_DEPTH = 2
|
15 | const PUSH_SUB_PATH = 3
|
16 |
|
17 |
|
18 | const BEFORE_PATH = 0
|
19 | const IN_PATH = 1
|
20 | const BEFORE_IDENT = 2
|
21 | const IN_IDENT = 3
|
22 | const IN_SUB_PATH = 4
|
23 | const IN_SINGLE_QUOTE = 5
|
24 | const IN_DOUBLE_QUOTE = 6
|
25 | const AFTER_PATH = 7
|
26 | const ERROR = 8
|
27 |
|
28 | const pathStateMachine: any = []
|
29 |
|
30 | pathStateMachine[BEFORE_PATH] = {
|
31 | 'ws': [BEFORE_PATH],
|
32 | 'ident': [IN_IDENT, APPEND],
|
33 | '[': [IN_SUB_PATH],
|
34 | 'eof': [AFTER_PATH]
|
35 | }
|
36 |
|
37 | pathStateMachine[IN_PATH] = {
|
38 | 'ws': [IN_PATH],
|
39 | '.': [BEFORE_IDENT],
|
40 | '[': [IN_SUB_PATH],
|
41 | 'eof': [AFTER_PATH]
|
42 | }
|
43 |
|
44 | pathStateMachine[BEFORE_IDENT] = {
|
45 | 'ws': [BEFORE_IDENT],
|
46 | 'ident': [IN_IDENT, APPEND],
|
47 | '0': [IN_IDENT, APPEND],
|
48 | 'number': [IN_IDENT, APPEND]
|
49 | }
|
50 |
|
51 | pathStateMachine[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 |
|
61 | pathStateMachine[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 |
|
70 | pathStateMachine[IN_SINGLE_QUOTE] = {
|
71 | "'": [IN_SUB_PATH, APPEND],
|
72 | 'eof': ERROR,
|
73 | 'else': [IN_SINGLE_QUOTE, APPEND]
|
74 | }
|
75 |
|
76 | pathStateMachine[IN_DOUBLE_QUOTE] = {
|
77 | '"': [IN_SUB_PATH, APPEND],
|
78 | 'eof': ERROR,
|
79 | 'else': [IN_DOUBLE_QUOTE, APPEND]
|
80 | }
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 | const literalValueRE: RegExp = /^\s?(?:true|false|-?[\d.]+|'[^']*'|"[^"]*")\s?$/
|
87 | function isLiteral (exp: string): boolean {
|
88 | return literalValueRE.test(exp)
|
89 | }
|
90 |
|
91 |
|
92 |
|
93 |
|
94 |
|
95 | function 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 |
|
105 |
|
106 |
|
107 | function 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 0x20:
|
126 | case 0x09:
|
127 | case 0x0A:
|
128 | case 0x0D:
|
129 | case 0xA0:
|
130 | case 0xFEFF:
|
131 | case 0x2028:
|
132 | case 0x2029:
|
133 | return 'ws'
|
134 | }
|
135 |
|
136 | return 'ident'
|
137 | }
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 | function formatSubPath (path: string): boolean | string {
|
146 | const trimmed: string = path.trim()
|
147 |
|
148 | if (path.charAt(0) === '0' && isNaN(path)) { return false }
|
149 |
|
150 | return isLiteral(trimmed) ? stripQuotes(trimmed) : '*' + trimmed
|
151 | }
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 | function parse (path: Path): ?Array<string> {
|
158 | const keys: Array<string> = []
|
159 | let index: number = -1
|
160 | let mode: number = BEFORE_PATH
|
161 | let subPathDepth: number = 0
|
162 | let c: ?string
|
163 | let key: any
|
164 | let newChar: any
|
165 | let type: string
|
166 | let transition: number
|
167 | let action: Function
|
168 | let typeMap: any
|
169 | const actions: Array<Function> = []
|
170 |
|
171 | actions[PUSH] = function () {
|
172 | if (key !== undefined) {
|
173 | keys.push(key)
|
174 | key = undefined
|
175 | }
|
176 | }
|
177 |
|
178 | actions[APPEND] = function () {
|
179 | if (key === undefined) {
|
180 | key = newChar
|
181 | } else {
|
182 | key += newChar
|
183 | }
|
184 | }
|
185 |
|
186 | actions[INC_SUB_PATH_DEPTH] = function () {
|
187 | actions[APPEND]()
|
188 | subPathDepth++
|
189 | }
|
190 |
|
191 | actions[PUSH_SUB_PATH] = function () {
|
192 | if (subPathDepth > 0) {
|
193 | subPathDepth--
|
194 | mode = IN_SUB_PATH
|
195 | actions[APPEND]()
|
196 | } else {
|
197 | subPathDepth = 0
|
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
|
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 |
|
252 | export type PathValue = PathValueObject | PathValueArray | string | number | boolean | null
|
253 | export type PathValueObject = { [key: string]: PathValue }
|
254 | export type PathValueArray = Array<PathValue>
|
255 |
|
256 | export default class I18nPath {
|
257 | _cache: Object
|
258 |
|
259 | constructor () {
|
260 | this._cache = Object.create(null)
|
261 | }
|
262 |
|
263 | |
264 |
|
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 |
|
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 | }
|