Skip to content

use a simple expression parser instead of regexp to parse directive #3734

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 23, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 167 additions & 59 deletions src/parsers/directive.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down Expand Up @@ -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)
Expand Down
26 changes: 26 additions & 0 deletions test/unit/specs/parsers/directive_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down