Skip to content

Commit 5ffc904

Browse files
committed
fix: named params for filters, working on #113
1 parent 08646f7 commit 5ffc904

File tree

10 files changed

+174
-132
lines changed

10 files changed

+174
-132
lines changed

package-lock.json

-15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/liquid.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import ITemplate from './template/itemplate'
66
import Tokenizer from './parser/tokenizer'
77
import Render from './render/render'
88
import Tag from './template/tag/tag'
9-
import Filter from './template/filter/filter'
9+
import { Filter } from './template/filter/filter'
1010
import Parser from './parser/parser'
1111
import ITagImplOptions from './template/tag/itag-impl-options'
1212
import Value from './template/value'

src/render/syntax.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export function evalExp (str: string, scope: Scope): any {
7777
return value instanceof Drop ? value.valueOf() : value
7878
}
7979

80-
function parseValue (str: string, scope: Scope): any {
80+
function parseValue (str: string | undefined, scope: Scope): any {
8181
if (!str) return null
8282
str = str.trim()
8383

@@ -91,7 +91,7 @@ function parseValue (str: string, scope: Scope): any {
9191
return scope.get(str)
9292
}
9393

94-
export function evalValue (str: string, scope: Scope): any {
94+
export function evalValue (str: string | undefined, scope: Scope): any {
9595
const value = parseValue(str, scope)
9696
return value instanceof Drop ? value.valueOf() : value
9797
}

src/template/filter/filter.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import { evalValue } from '../../render/syntax'
22
import Scope from '../../scope/scope'
3+
import { isArray } from '../../util/underscore'
34
import { FilterImpl } from './filter-impl'
45

5-
export default class Filter {
6+
export type FilterArgs = Array<string|[string?, string?]>
7+
8+
export class Filter {
69
name: string
710
impl: FilterImpl
8-
args: string[]
11+
args: FilterArgs
912
private static impls: {[key: string]: FilterImpl} = {}
1013

11-
constructor (name: string, args: string[], strictFilters: boolean) {
14+
constructor (name: string, args: FilterArgs, strictFilters: boolean) {
1215
const impl = Filter.impls[name]
1316
if (!impl && strictFilters) throw new TypeError(`undefined filter: ${name}`)
1417

@@ -17,7 +20,7 @@ export default class Filter {
1720
this.args = args
1821
}
1922
render (value: any, scope: Scope): any {
20-
const args = this.args.map(arg => evalValue(arg, scope))
23+
const args = this.args.map(arg => isArray(arg) ? [arg[0], evalValue(arg[1], scope)] : evalValue(arg, scope))
2124
return this.impl.apply(null, [value, ...args])
2225
}
2326
static register (name: string, filter: FilterImpl) {

src/template/value.ts

+56-60
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,76 @@
11
import { evalExp } from '../render/syntax'
2-
import Filter from './filter/filter'
2+
import { FilterArgs, Filter } from './filter/filter'
33
import Scope from '../scope/scope'
44

5-
enum ParseState {
6-
INIT = 0,
7-
FILTER_NAME = 1,
8-
FILTER_ARG = 2
9-
}
10-
11-
export default class {
12-
initial: any
5+
export default class Value {
6+
private strictFilters: boolean
7+
initial: string
138
filters: Array<Filter> = []
149

1510
/**
1611
* @param str value string, like: "i have a dream | truncate: 3
1712
*/
1813
constructor (str: string, strictFilters: boolean) {
19-
let buffer = ''
20-
let quoted = ''
21-
let state = ParseState.INIT
22-
let sealed = false
23-
24-
let filterName = ''
25-
let filterArgs: string[] = []
26-
27-
for (let i = 0; i < str.length; i++) {
28-
if (quoted) {
29-
if (str[i] === quoted) {
30-
quoted = ''
31-
sealed = true
32-
}
33-
buffer += str[i]
34-
} else if (/\s/.test(str[i])) {
35-
if (!buffer) continue
36-
else sealed = true
37-
} else if (str[i] === '|') {
38-
if (state === ParseState.INIT) {
39-
this.initial = buffer
40-
} else {
41-
if (state === ParseState.FILTER_NAME) filterName = buffer
42-
else filterArgs.push(buffer)
43-
this.filters.push(new Filter(filterName, filterArgs, strictFilters))
44-
filterName = ''
45-
filterArgs = []
46-
}
47-
state = ParseState.FILTER_NAME
48-
buffer = ''
49-
sealed = false
50-
} else if (state === ParseState.FILTER_NAME && str[i] === ':') {
51-
filterName = buffer
52-
state = ParseState.FILTER_ARG
53-
buffer = ''
54-
sealed = false
55-
} else if (state === ParseState.FILTER_ARG && str[i] === ',') {
56-
filterArgs.push(buffer)
57-
buffer = ''
58-
sealed = false
59-
} else if (sealed) continue
60-
else {
61-
if ((str[i] === '"' || str[i] === "'") && !quoted) quoted = str[i]
62-
buffer += str[i]
14+
const tokens = Value.tokenize(str)
15+
this.strictFilters = strictFilters
16+
this.initial = tokens[0]
17+
this.parseFilters(tokens, 1)
18+
}
19+
private parseFilters (tokens: string[], begin: number) {
20+
let i = begin
21+
while (i < tokens.length) {
22+
if (tokens[i] !== '|') {
23+
i++
24+
continue
6325
}
26+
const j = ++i
27+
while (i < tokens.length && tokens[i] !== '|') i++
28+
this.parseFilter(tokens, j, i)
6429
}
65-
66-
if (buffer) {
67-
if (state === ParseState.INIT) this.initial = buffer
68-
else if (state === ParseState.FILTER_NAME) this.filters.push(new Filter(buffer, [], strictFilters))
69-
else {
70-
filterArgs.push(buffer)
71-
this.filters.push(new Filter(filterName, filterArgs, strictFilters))
30+
}
31+
private parseFilter (tokens: string[], begin: number, end: number) {
32+
const name = tokens[begin]
33+
const args: FilterArgs = []
34+
let argName, argValue
35+
for (let i = begin + 1; i < end + 1; i++) {
36+
if (i === end || tokens[i] === ',') {
37+
if (argName || argValue) {
38+
args.push(argName ? [argName, argValue] : <string>argValue)
39+
}
40+
argValue = argName = undefined
41+
} else if (tokens[i] === ':') {
42+
argName = argValue
43+
argValue = undefined
44+
} else if (argValue === undefined) {
45+
argValue = tokens[i]
7246
}
7347
}
48+
this.filters.push(new Filter(name, args, this.strictFilters))
7449
}
7550
value (scope: Scope) {
7651
return this.filters.reduce(
7752
(prev, filter) => filter.render(prev, scope),
7853
evalExp(this.initial, scope))
7954
}
55+
static tokenize (str: string): Array<'|' | ',' | ':' | string> {
56+
const tokens = []
57+
let i = 0
58+
while (i < str.length) {
59+
const ch = str[i]
60+
if (ch === '"' || ch === "'") {
61+
const j = i
62+
for (i += 2; i < str.length && str[i - 1] !== ch; ++i);
63+
tokens.push(str.slice(j, i))
64+
} else if (/\s/.test(ch)) {
65+
i++
66+
} else if (/[|,:]/.test(ch)) {
67+
tokens.push(str[i++])
68+
} else {
69+
const j = i++
70+
for (; i < str.length && !/[|,:\s]/.test(str[i]); ++i);
71+
tokens.push(str.slice(j, i))
72+
}
73+
}
74+
return tokens
75+
}
8076
}
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { test, liquid } from '../../../stub/render'
2+
3+
describe('filters/custom', function () {
4+
liquid.registerFilter('obj_test', function () {
5+
return JSON.stringify(arguments)
6+
})
7+
it('should support object', () => test(
8+
`{{ "a" | obj_test: k1: "v1", k2: foo }}`,
9+
'{"0":"a","1":["k1","v1"],"2":["k2","bar"]}'
10+
))
11+
it('should support mixed object', () => test(
12+
`{{ "a" | obj_test: "something", k1: "v1", k2: foo }}`,
13+
'{"0":"a","1":"something","2":["k1","v1"],"3":["k2","bar"]}'
14+
))
15+
})

test/unit/render/render.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { expect } from 'chai'
22
import Scope from '../../../src/scope/scope'
33
import Token from '../../../src/parser/token'
44
import Tag from '../../../src/template/tag/tag'
5-
import Filter from '../../../src/template/filter/filter'
5+
import { Filter } from '../../../src/template/filter/filter'
66
import Render from '../../../src/render/render'
77
import HTML from '../../../src/template/html'
88

test/unit/template/filter/filter.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as chai from 'chai'
22
import * as sinon from 'sinon'
33
import * as sinonChai from 'sinon-chai'
4-
import Filter from '../../../../src/template/filter/filter'
4+
import { Filter } from '../../../../src/template/filter/filter'
55
import Scope from '../../../../src/scope/scope'
66

77
chai.use(sinonChai)

test/unit/template/output.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as chai from 'chai'
22
import Scope from '../../../src/scope/scope'
33
import Output from '../../../src/template/output'
44
import OutputToken from '../../../src/parser/output-token'
5-
import Filter from '../../../src/template/filter/filter'
5+
import { Filter } from '../../../src/template/filter/filter'
66

77
const expect = chai.expect
88

0 commit comments

Comments
 (0)