Skip to content

Commit

Permalink
⚡ improvement(keypath): port the object path parser
Browse files Browse the repository at this point in the history
  • Loading branch information
kazupon committed May 7, 2016
1 parent 5344b51 commit 3ae04b7
Show file tree
Hide file tree
Showing 5 changed files with 344 additions and 9 deletions.
13 changes: 5 additions & 8 deletions src/extend.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import format from './format'
import compare from './compare'
import { getValue } from './path'


/**
Expand All @@ -10,16 +10,13 @@ import compare from './compare'
*/

export default function (Vue) {
const getPath = (compare('1.0.8', Vue.version) === -1)
? Vue.parsers.path.getPath
: Vue.parsers.path.get
const util = Vue.util
const { isArray, isObject } = Vue.util

function getVal (key, lang, args) {
let value = key
try {
let locale = Vue.locale(lang)
let val = getPath(locale, key) || locale[key]
let val = getValue(locale, key) || locale[key]
value = (args ? format(val, args) : val) || key
} catch (e) {
value = key
Expand All @@ -41,7 +38,7 @@ export default function (Vue) {

let language = Vue.config.lang
if (args.length === 1) {
if (util.isObject(args[0]) || util.isArray(args[0])) {
if (isObject(args[0]) || isArray(args[0])) {
args = args[0]
} else if (typeof args[0] === 'string') {
language = args[0]
Expand All @@ -50,7 +47,7 @@ export default function (Vue) {
if (typeof args[0] === 'string') {
language = args[0]
}
if (util.isObject(args[1]) || util.isArray(args[1])) {
if (isObject(args[1]) || isArray(args[1])) {
args = args[1]
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import util, { warn, empty, each } from './util'
import path from './path'
import compare from './compare'
import Asset from './asset'
import Override from './override'
Expand Down Expand Up @@ -39,7 +40,7 @@ function plugin (Vue, opts = {}) {
}
let locales = opts.locales || {}

util.Vue = Vue
path.Vue = util.Vue = Vue
setupLangVM(Vue, lang)

Asset(Vue)
Expand Down
300 changes: 300 additions & 0 deletions src/path.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
/**
* Path paerser
* - Inspired:
* Vue.js Path parser
*/

import { empty } from './util'


// export default for holding the Vue reference
const exports = {}
export default exports

// cache
let pathCache = Object.create(null)

// 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

let pathStateMachine = []

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]
}

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]
}

/**
* Determine the type of a character in a keypath.
*
* @param {Char} ch
* @return {String} type
*/

function getPathCharType (ch) {
if (ch === undefined) { return 'eof' }

let code = ch.charCodeAt(0)

switch (code) {
case 0x5B: // [
case 0x5D: // ]
case 0x2E: // .
case 0x22: // "
case 0x27: // '
case 0x30: // 0
return ch

case 0x5F: // _
case 0x24: // $
return 'ident'

case 0x20: // Space
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'
}

// a-z, A-Z
if ((code >= 0x61 && code <= 0x7A) || (code >= 0x41 && code <= 0x5A)) {
return 'ident'
}

// 1-9
if (code >= 0x31 && code <= 0x39) { return 'number' }

return 'else'
}

/**
* Format a subPath, return its plain form if it is
* a literal string or number. Otherwise prepend the
* dynamic indicator (*).
*
* @param {String} path
* @return {String}
*/

function formatSubPath (path) {
const { isLiteral, stripQuotes } = exports.Vue.util

let trimmed = 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
*
* @param {String} path
* @return {Array|undefined}
*/

function parse (path) {
let keys = []
let index = -1
let mode = BEFORE_PATH
let subPathDepth = 0
let c, newChar, key, type, transition, action, typeMap

let actions = []

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
key = formatSubPath(key)
if (key === false) {
return false
} else {
actions[PUSH]()
}
}
}

function maybeUnescapeQuote () {
let nextChar = 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) {
keys.raw = path
return keys
}
}
}

/**
* External parse that check for a cache hit first
*
* @param {String} path
* @return {Array|undefined}
*/

function parsePath (path) {
let hit = pathCache[path]
if (!hit) {
hit = parse(path)
if (hit) {
pathCache[path] = hit
}
}
return hit
}

/**
* Get value from path string
*
* @param {Object} obj
* @param {String} path
* @return value
*/

export function getValue (obj, path) {
const { isObject } = exports.Vue.util

if (!isObject(obj)) { return null }

let paths = parsePath(path)
if (empty(paths)) { return null }

let ret = null
let last = obj
let length = paths.length
let i = 0
while (i < length) {
let value = last[paths[i]]
if (value === undefined) {
last = null
break
}
last = value
i++
}

ret = last
return ret
}
1 change: 1 addition & 0 deletions test/specs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import locales from './fixture/locales'
import plugin from '../../src/index'
import 'babel-polyfill'

require('./path')
require('./format')
require('./compare')

Expand Down
Loading

0 comments on commit 3ae04b7

Please sign in to comment.