Skip to content

Commit b2ab9fa

Browse files
defccyyx990803
authored andcommitted
use a simple expression parser instead of regexp to parse directive (vuejs#3734)
1 parent 2f35c99 commit b2ab9fa

File tree

2 files changed

+193
-59
lines changed

2 files changed

+193
-59
lines changed

src/parsers/directive.js

Lines changed: 167 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,173 @@ import { toNumber, stripQuotes } from '../util/index'
22
import Cache from '../cache'
33

44
const cache = new Cache(1000)
5-
const filterTokenRE = /[^\s'"]+|'[^']*'|"[^"]*"/g
65
const reservedArgRE = /^in$|^-?\d+/
76

87
/**
98
* Parser state
109
*/
1110

12-
var str, dir
13-
var c, prev, i, l, lastFilterIndex
14-
var inSingle, inDouble, curly, square, paren
11+
var str, dir, len
12+
var index
13+
var chr
14+
var state
15+
var startState = 0
16+
var filterState = 1
17+
var filterNameState = 2
18+
var filterArgState = 3
19+
20+
var doubleChr = 0x22
21+
var singleChr = 0x27
22+
var pipeChr = 0x7C
23+
var escapeChr = 0x5C
24+
var spaceChr = 0x20
25+
26+
var expStartChr = { 0x5B: 1, 0x7B: 1, 0x28: 1 }
27+
var expChrPair = { 0x5B: 0x5D, 0x7B: 0x7D, 0x28: 0x29 }
28+
29+
function peek () {
30+
return str.charCodeAt(index + 1)
31+
}
32+
33+
function next () {
34+
return str.charCodeAt(++index)
35+
}
36+
37+
function eof () {
38+
return index >= len
39+
}
40+
41+
function eatSpace () {
42+
while (peek() === spaceChr) {
43+
next()
44+
}
45+
}
46+
47+
function isStringStart (chr) {
48+
return chr === doubleChr || chr === singleChr
49+
}
50+
51+
function isExpStart (chr) {
52+
return expStartChr[chr]
53+
}
54+
55+
function isExpEnd (start, chr) {
56+
return expChrPair[start] === chr
57+
}
58+
59+
function parseString () {
60+
var stringQuote = next()
61+
var chr
62+
while (!eof()) {
63+
chr = next()
64+
// escape char
65+
if (chr === escapeChr) {
66+
next()
67+
} else if (chr === stringQuote) {
68+
break
69+
}
70+
}
71+
}
72+
73+
function parseSpecialExp (chr) {
74+
var inExp = 0
75+
var startChr = chr
76+
77+
while (!eof()) {
78+
chr = peek()
79+
if (isStringStart(chr)) {
80+
parseString()
81+
continue
82+
}
83+
84+
if (startChr === chr) {
85+
inExp++
86+
}
87+
if (isExpEnd(startChr, chr)) {
88+
inExp--
89+
}
90+
91+
next()
92+
93+
if (inExp === 0) {
94+
break
95+
}
96+
}
97+
}
1598

1699
/**
17-
* Push a filter to the current directive object
100+
* syntax:
101+
* expression | filterName [arg arg [| filterName arg arg]]
18102
*/
19103

20-
function pushFilter () {
21-
var exp = str.slice(lastFilterIndex, i).trim()
22-
var filter
23-
if (exp) {
24-
filter = {}
25-
var tokens = exp.match(filterTokenRE)
26-
filter.name = tokens[0]
27-
if (tokens.length > 1) {
28-
filter.args = tokens.slice(1).map(processFilterArg)
104+
function parseExpression () {
105+
var start = index
106+
while (!eof()) {
107+
chr = peek()
108+
if (isStringStart(chr)) {
109+
parseString()
110+
} else if (isExpStart(chr)) {
111+
parseSpecialExp(chr)
112+
} else if (chr === pipeChr) {
113+
next()
114+
chr = peek()
115+
if (chr === pipeChr) {
116+
next()
117+
} else {
118+
if (state === startState || state === filterArgState) {
119+
state = filterState
120+
}
121+
break
122+
}
123+
} else if (chr === spaceChr && (state === filterNameState || state === filterArgState)) {
124+
eatSpace()
125+
break
126+
} else {
127+
if (state === filterState) {
128+
state = filterNameState
129+
}
130+
next()
29131
}
30132
}
31-
if (filter) {
32-
(dir.filters = dir.filters || []).push(filter)
133+
134+
return str.slice(start + 1, index) || null
135+
}
136+
137+
function parseFilterList () {
138+
var filters = []
139+
while (!eof()) {
140+
filters.push(parseFilter())
33141
}
34-
lastFilterIndex = i + 1
142+
return filters
143+
}
144+
145+
function parseFilter () {
146+
var filter = {}
147+
var args
148+
149+
state = filterState
150+
filter.name = parseExpression().trim()
151+
152+
state = filterArgState
153+
args = parseFilterArguments()
154+
155+
if (args.length) {
156+
filter.args = args
157+
}
158+
return filter
159+
}
160+
161+
function parseFilterArguments () {
162+
var args = []
163+
while (!eof() && state !== filterState) {
164+
var arg = parseExpression()
165+
if (!arg) {
166+
break
167+
}
168+
args.push(processFilterArg(arg))
169+
}
170+
171+
return args
35172
}
36173

37174
/**
@@ -83,51 +220,22 @@ export function parseDirective (s) {
83220

84221
// reset parser state
85222
str = s
86-
inSingle = inDouble = false
87-
curly = square = paren = 0
88-
lastFilterIndex = 0
89223
dir = {}
224+
len = str.length
225+
index = -1
226+
chr = ''
227+
state = startState
90228

91-
for (i = 0, l = str.length; i < l; i++) {
92-
prev = c
93-
c = str.charCodeAt(i)
94-
if (inSingle) {
95-
// check single quote
96-
if (c === 0x27 && prev !== 0x5C) inSingle = !inSingle
97-
} else if (inDouble) {
98-
// check double quote
99-
if (c === 0x22 && prev !== 0x5C) inDouble = !inDouble
100-
} else if (
101-
c === 0x7C && // pipe
102-
str.charCodeAt(i + 1) !== 0x7C &&
103-
str.charCodeAt(i - 1) !== 0x7C
104-
) {
105-
if (dir.expression == null) {
106-
// first filter, end of expression
107-
lastFilterIndex = i + 1
108-
dir.expression = str.slice(0, i).trim()
109-
} else {
110-
// already has filter
111-
pushFilter()
112-
}
113-
} else {
114-
switch (c) {
115-
case 0x22: inDouble = true; break // "
116-
case 0x27: inSingle = true; break // '
117-
case 0x28: paren++; break // (
118-
case 0x29: paren--; break // )
119-
case 0x5B: square++; break // [
120-
case 0x5D: square--; break // ]
121-
case 0x7B: curly++; break // {
122-
case 0x7D: curly--; break // }
123-
}
124-
}
125-
}
229+
var filters
126230

127-
if (dir.expression == null) {
128-
dir.expression = str.slice(0, i).trim()
129-
} else if (lastFilterIndex !== 0) {
130-
pushFilter()
231+
if (str.indexOf('|') < 0) {
232+
dir.expression = str.trim()
233+
} else {
234+
dir.expression = parseExpression().trim()
235+
filters = parseFilterList()
236+
if (filters.length) {
237+
dir.filters = filters
238+
}
131239
}
132240

133241
cache.put(s, dir)

test/unit/specs/parsers/directive_spec.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,32 @@ describe('Directive Parser', function () {
8585
expect(res.filters[0].args).toBeUndefined()
8686
})
8787

88+
it('white spaces inside object literal', function () {
89+
var res = parse('abc | filter {a:1} {b: 2}')
90+
expect(res.expression).toBe('abc')
91+
expect(res.filters.length).toBe(1)
92+
expect(res.filters[0].name).toBe('filter')
93+
expect(res.filters[0].args.length).toBe(2)
94+
expect(res.filters[0].args[0].value).toBe('{a:1}')
95+
expect(res.filters[0].args[0].dynamic).toBe(true)
96+
expect(res.filters[0].args[1].value).toBe('{b: 2}')
97+
expect(res.filters[0].args[1].dynamic).toBe(true)
98+
})
99+
100+
it('white spaces inside array literal', function () {
101+
var res = parse('abc | filter0 abc||def | filter1 [ 1, { a: 2 }]')
102+
expect(res.expression).toBe('abc')
103+
expect(res.filters.length).toBe(2)
104+
expect(res.filters[0].name).toBe('filter0')
105+
expect(res.filters[0].args.length).toBe(1)
106+
expect(res.filters[0].args[0].value).toBe('abc||def')
107+
expect(res.filters[0].args[0].dynamic).toBe(true)
108+
expect(res.filters[1].name).toBe('filter1')
109+
expect(res.filters[1].args.length).toBe(1)
110+
expect(res.filters[1].args[0].value).toBe('[ 1, { a: 2 }]')
111+
expect(res.filters[1].args[0].dynamic).toBe(true)
112+
})
113+
88114
it('cache', function () {
89115
var res1 = parse('a || b | c')
90116
var res2 = parse('a || b | c')

0 commit comments

Comments
 (0)