Repository: vuejs/vue
Pull request: use a simple expression parser instead of regexp to parse directive

Use a state machine based expression parser to parse the directive.

 fix #3210 and #3214 #


Changed files:
filename: src/parsers/directive.js
patch: @@ -2,36 +2,173 @@ import { toNumber, stripQuotes } from '../util/index'
 import Cache from '../cache'
 
 const cache = new Cache(1000)
-const filterTokenRE = /[^\s'"]+|'[^']*'|"[^"]*"/g
 const reservedArgRE = /^in$|^-?\d+/
 
 /**
  * Parser state
  */
 
-var str, dir
-var c, prev, i, l, lastFilterIndex
-var inSingle, inDouble, curly, square, paren
+var str, dir, len
+var index
+var chr
+var state
+var startState = 0
+var filterState = 1
+var filterNameState = 2
+var filterArgState = 3
+
+var doubleChr = 0x22
+var singleChr = 0x27
+var pipeChr = 0x7C
+var escapeChr = 0x5C
+var spaceChr = 0x20
+
+var expStartChr = { 0x5B: 1, 0x7B: 1, 0x28: 1 }
+var expChrPair = { 0x5B: 0x5D, 0x7B: 0x7D, 0x28: 0x29 }
+
+function peek () {
+  return str.charCodeAt(index + 1)
+}
+
+function next () {
+  return str.charCodeAt(++index)
+}
+
+function eof () {
+  return index >= len
+}
+
+function eatSpace () {
+  while (peek() === spaceChr) {
+    next()
+  }
+}
+
+function isStringStart (chr) {
+  return chr === doubleChr || chr === singleChr
+}
+
+function isExpStart (chr) {
+  return expStartChr[chr]
+}
+
+function isExpEnd (start, chr) {
+  return expChrPair[start] === chr
+}
+
+function parseString () {
+  var stringQuote = next()
+  var chr
+  while (!eof()) {
+    chr = next()
+    // escape char
+    if (chr === escapeChr) {
+      next()
+    } else if (chr === stringQuote) {
+      break
+    }
+  }
+}
+
+function parseSpecialExp (chr) {
+  var inExp = 0
+  var startChr = chr
+
+  while (!eof()) {
+    chr = peek()
+    if (isStringStart(chr)) {
+      parseString()
+      continue
+    }
+
+    if (startChr === chr) {
+      inExp++
+    }
+    if (isExpEnd(startChr, chr)) {
+      inExp--
+    }
+
+    next()
+
+    if (inExp === 0) {
+      break
+    }
+  }
+}
 
 /**
- * Push a filter to the current directive object
+ * syntax:
+ * expression | filterName  [arg  arg [| filterName arg arg]]
  */
 
-function pushFilter () {
-  var exp = str.slice(lastFilterIndex, i).trim()
-  var filter
-  if (exp) {
-    filter = {}
-    var tokens = exp.match(filterTokenRE)
-    filter.name = tokens[0]
-    if (tokens.length > 1) {
-      filter.args = tokens.slice(1).map(processFilterArg)
+function parseExpression () {
+  var start = index
+  while (!eof()) {
+    chr = peek()
+    if (isStringStart(chr)) {
+      parseString()
+    } else if (isExpStart(chr)) {
+      parseSpecialExp(chr)
+    } else if (chr === pipeChr) {
+      next()
+      chr = peek()
+      if (chr === pipeChr) {
+        next()
+      } else {
+        if (state === startState || state === filterArgState) {
+          state = filterState
+        }
+        break
+      }
+    } else if (chr === spaceChr && (state === filterNameState || state === filterArgState)) {
+      eatSpace()
+      break
+    } else {
+      if (state === filterState) {
+        state = filterNameState
+      }
+      next()
     }
   }
-  if (filter) {
-    (dir.filters = dir.filters || []).push(filter)
+
+  return str.slice(start + 1, index) || null
+}
+
+function parseFilterList () {
+  var filters = []
+  while (!eof()) {
+    filters.push(parseFilter())
   }
-  lastFilterIndex = i + 1
+  return filters
+}
+
+function parseFilter () {
+  var filter = {}
+  var args
+
+  state = filterState
+  filter.name = parseExpression().trim()
+
+  state = filterArgState
+  args = parseFilterArguments()
+
+  if (args.length) {
+    filter.args = args
+  }
+  return filter
+}
+
+function parseFilterArguments () {
+  var args = []
+  while (!eof() && state !== filterState) {
+    var arg = parseExpression()
+    if (!arg) {
+      break
+    }
+    args.push(processFilterArg(arg))
+  }
+
+  return args
 }
 
 /**
@@ -83,51 +220,22 @@ export function parseDirective (s) {
 
   // reset parser state
   str = s
-  inSingle = inDouble = false
-  curly = square = paren = 0
-  lastFilterIndex = 0
   dir = {}
+  len = str.length
+  index = -1
+  chr = ''
+  state = startState
 
-  for (i = 0, l = str.length; i < l; i++) {
-    prev = c
-    c = str.charCodeAt(i)
-    if (inSingle) {
-      // check single quote
-      if (c === 0x27 && prev !== 0x5C) inSingle = !inSingle
-    } else if (inDouble) {
-      // check double quote
-      if (c === 0x22 && prev !== 0x5C) inDouble = !inDouble
-    } else if (
-      c === 0x7C && // pipe
-      str.charCodeAt(i + 1) !== 0x7C &&
-      str.charCodeAt(i - 1) !== 0x7C
-    ) {
-      if (dir.expression == null) {
-        // first filter, end of expression
-        lastFilterIndex = i + 1
-        dir.expression = str.slice(0, i).trim()
-      } else {
-        // already has filter
-        pushFilter()
-      }
-    } else {
-      switch (c) {
-        case 0x22: inDouble = true; break // "
-        case 0x27: inSingle = true; break // '
-        case 0x28: paren++; break         // (
-        case 0x29: paren--; break         // )
-        case 0x5B: square++; break        // [
-        case 0x5D: square--; break        // ]
-        case 0x7B: curly++; break         // {
-        case 0x7D: curly--; break         // }
-      }
-    }
-  }
+  var filters
 
-  if (dir.expression == null) {
-    dir.expression = str.slice(0, i).trim()
-  } else if (lastFilterIndex !== 0) {
-    pushFilter()
+  if (str.indexOf('|') < 0) {
+    dir.expression = str.trim()
+  } else {
+    dir.expression = parseExpression().trim()
+    filters = parseFilterList()
+    if (filters.length) {
+      dir.filters = filters
+    }
   }
 
   cache.put(s, dir)

filename: test/unit/specs/parsers/directive_spec.js
patch: @@ -85,6 +85,32 @@ describe('Directive Parser', function () {
     expect(res.filters[0].args).toBeUndefined()
   })
 
+  it('white spaces inside object literal', function () {
+    var res = parse('abc | filter {a:1} {b: 2}')
+    expect(res.expression).toBe('abc')
+    expect(res.filters.length).toBe(1)
+    expect(res.filters[0].name).toBe('filter')
+    expect(res.filters[0].args.length).toBe(2)
+    expect(res.filters[0].args[0].value).toBe('{a:1}')
+    expect(res.filters[0].args[0].dynamic).toBe(true)
+    expect(res.filters[0].args[1].value).toBe('{b: 2}')
+    expect(res.filters[0].args[1].dynamic).toBe(true)
+  })
+
+  it('white spaces inside array literal', function () {
+    var res = parse('abc | filter0 abc||def | filter1 [ 1, { a: 2 }]')
+    expect(res.expression).toBe('abc')
+    expect(res.filters.length).toBe(2)
+    expect(res.filters[0].name).toBe('filter0')
+    expect(res.filters[0].args.length).toBe(1)
+    expect(res.filters[0].args[0].value).toBe('abc||def')
+    expect(res.filters[0].args[0].dynamic).toBe(true)
+    expect(res.filters[1].name).toBe('filter1')
+    expect(res.filters[1].args.length).toBe(1)
+    expect(res.filters[1].args[0].value).toBe('[ 1, { a: 2 }]')
+    expect(res.filters[1].args[0].dynamic).toBe(true)
+  })
+
   it('cache', function () {
     var res1 = parse('a || b | c')
     var res2 = parse('a || b | c')