123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302 |
- /* @flow */
- import { isObject } from './util'
- /**
- * Path parser
- * - Inspired:
- * Vue.js Path parser
- */
- // actions
- const APPEND = 0
- const PUSH = 1
- const INC_SUB_PATH_DEPTH = 2
- const PUSH_SUB_PATH = 3
- // states
- const BEFORE_PATH = 0
- const IN_PATH = 1
- const BEFORE_IDENT = 2
- const IN_IDENT = 3
- const IN_SUB_PATH = 4
- const IN_SINGLE_QUOTE = 5
- const IN_DOUBLE_QUOTE = 6
- const AFTER_PATH = 7
- const ERROR = 8
- const pathStateMachine: any = []
- pathStateMachine[BEFORE_PATH] = {
- 'ws': [BEFORE_PATH],
- 'ident': [IN_IDENT, APPEND],
- '[': [IN_SUB_PATH],
- 'eof': [AFTER_PATH]
- }
- pathStateMachine[IN_PATH] = {
- 'ws': [IN_PATH],
- '.': [BEFORE_IDENT],
- '[': [IN_SUB_PATH],
- 'eof': [AFTER_PATH]
- }
- pathStateMachine[BEFORE_IDENT] = {
- 'ws': [BEFORE_IDENT],
- 'ident': [IN_IDENT, APPEND],
- '0': [IN_IDENT, APPEND],
- 'number': [IN_IDENT, APPEND]
- }
- pathStateMachine[IN_IDENT] = {
- 'ident': [IN_IDENT, APPEND],
- '0': [IN_IDENT, APPEND],
- 'number': [IN_IDENT, APPEND],
- 'ws': [IN_PATH, PUSH],
- '.': [BEFORE_IDENT, PUSH],
- '[': [IN_SUB_PATH, PUSH],
- 'eof': [AFTER_PATH, PUSH]
- }
- pathStateMachine[IN_SUB_PATH] = {
- "'": [IN_SINGLE_QUOTE, APPEND],
- '"': [IN_DOUBLE_QUOTE, APPEND],
- '[': [IN_SUB_PATH, INC_SUB_PATH_DEPTH],
- ']': [IN_PATH, PUSH_SUB_PATH],
- 'eof': ERROR,
- 'else': [IN_SUB_PATH, APPEND]
- }
- pathStateMachine[IN_SINGLE_QUOTE] = {
- "'": [IN_SUB_PATH, APPEND],
- 'eof': ERROR,
- 'else': [IN_SINGLE_QUOTE, APPEND]
- }
- pathStateMachine[IN_DOUBLE_QUOTE] = {
- '"': [IN_SUB_PATH, APPEND],
- 'eof': ERROR,
- 'else': [IN_DOUBLE_QUOTE, APPEND]
- }
- /**
- * Check if an expression is a literal value.
- */
- const literalValueRE: RegExp = /^\s?(?:true|false|-?[\d.]+|'[^']*'|"[^"]*")\s?$/
- function isLiteral (exp: string): boolean {
- return literalValueRE.test(exp)
- }
- /**
- * Strip quotes from a string
- */
- function stripQuotes (str: string): string | boolean {
- const a: number = str.charCodeAt(0)
- const b: number = str.charCodeAt(str.length - 1)
- return a === b && (a === 0x22 || a === 0x27)
- ? str.slice(1, -1)
- : str
- }
- /**
- * Determine the type of a character in a keypath.
- */
- function getPathCharType (ch: ?string): string {
- if (ch === undefined || ch === null) { return 'eof' }
- const code: number = ch.charCodeAt(0)
- switch (code) {
- case 0x5B: // [
- case 0x5D: // ]
- case 0x2E: // .
- case 0x22: // "
- case 0x27: // '
- return ch
- case 0x5F: // _
- case 0x24: // $
- case 0x2D: // -
- return 'ident'
- case 0x09: // Tab
- case 0x0A: // Newline
- case 0x0D: // Return
- case 0xA0: // No-break space
- case 0xFEFF: // Byte Order Mark
- case 0x2028: // Line Separator
- case 0x2029: // Paragraph Separator
- return 'ws'
- }
- return 'ident'
- }
- /**
- * Format a subPath, return its plain form if it is
- * a literal string or number. Otherwise prepend the
- * dynamic indicator (*).
- */
- function formatSubPath (path: string): boolean | string {
- const trimmed: string = path.trim()
- // invalid leading 0
- if (path.charAt(0) === '0' && isNaN(path)) { return false }
- return isLiteral(trimmed) ? stripQuotes(trimmed) : '*' + trimmed
- }
- /**
- * Parse a string path into an array of segments
- */
- function parse (path: Path): ?Array<string> {
- const keys: Array<string> = []
- let index: number = -1
- let mode: number = BEFORE_PATH
- let subPathDepth: number = 0
- let c: ?string
- let key: any
- let newChar: any
- let type: string
- let transition: number
- let action: Function
- let typeMap: any
- const actions: Array<Function> = []
- actions[PUSH] = function () {
- if (key !== undefined) {
- keys.push(key)
- key = undefined
- }
- }
- actions[APPEND] = function () {
- if (key === undefined) {
- key = newChar
- } else {
- key += newChar
- }
- }
- actions[INC_SUB_PATH_DEPTH] = function () {
- actions[APPEND]()
- subPathDepth++
- }
- actions[PUSH_SUB_PATH] = function () {
- if (subPathDepth > 0) {
- subPathDepth--
- mode = IN_SUB_PATH
- actions[APPEND]()
- } else {
- subPathDepth = 0
- if (key === undefined) { return false }
- key = formatSubPath(key)
- if (key === false) {
- return false
- } else {
- actions[PUSH]()
- }
- }
- }
- function maybeUnescapeQuote (): ?boolean {
- const nextChar: string = path[index + 1]
- if ((mode === IN_SINGLE_QUOTE && nextChar === "'") ||
- (mode === IN_DOUBLE_QUOTE && nextChar === '"')) {
- index++
- newChar = '\\' + nextChar
- actions[APPEND]()
- return true
- }
- }
- while (mode !== null) {
- index++
- c = path[index]
- if (c === '\\' && maybeUnescapeQuote()) {
- continue
- }
- type = getPathCharType(c)
- typeMap = pathStateMachine[mode]
- transition = typeMap[type] || typeMap['else'] || ERROR
- if (transition === ERROR) {
- return // parse error
- }
- mode = transition[0]
- action = actions[transition[1]]
- if (action) {
- newChar = transition[2]
- newChar = newChar === undefined
- ? c
- : newChar
- if (action() === false) {
- return
- }
- }
- if (mode === AFTER_PATH) {
- return keys
- }
- }
- }
- export type PathValue = PathValueObject | PathValueArray | Function | string | number | boolean | null
- export type PathValueObject = { [key: string]: PathValue }
- export type PathValueArray = Array<PathValue>
- export default class I18nPath {
- _cache: Object
- constructor () {
- this._cache = Object.create(null)
- }
- /**
- * External parse that check for a cache hit first
- */
- parsePath (path: Path): Array<string> {
- let hit: ?Array<string> = this._cache[path]
- if (!hit) {
- hit = parse(path)
- if (hit) {
- this._cache[path] = hit
- }
- }
- return hit || []
- }
- /**
- * Get path value from path string
- */
- getPathValue (obj: mixed, path: Path): PathValue {
- if (!isObject(obj)) { return null }
- const paths: Array<string> = this.parsePath(path)
- if (paths.length === 0) {
- return null
- } else {
- const length: number = paths.length
- let last: any = obj
- let i: number = 0
- while (i < length) {
- const value: any = last[paths[i]]
- if (value === undefined || value === null) {
- return null
- }
- last = value
- i++
- }
- return last
- }
- }
- }
|