Skip to content

Commit 564ccb7

Browse files
committed
added filter to apply dynamic filter
1 parent 39ed9aa commit 564ccb7

File tree

12 files changed

+129
-70
lines changed

12 files changed

+129
-70
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
[Ilya Puchka](https://github.com/yonaskolb)
1313
[#178](https://github.com/stencilproject/Stencil/pull/178)
1414

15+
- Added support for dynamic filter using `filter` filter
16+
1517
### Bug Fixes
1618

1719
- Fixed using quote as a filter parameter

Sources/Expression.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
protocol Expression: CustomStringConvertible {
1+
public protocol Expression: CustomStringConvertible {
22
func evaluate(context: Context) throws -> Bool
33
}
44

Sources/Extension.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ open class Extension {
2626

2727
/// Registers a template filter with the given name
2828
public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?]) throws -> Any?) {
29+
filters[name] = .arguments({ value, args, _ in try filter(value, args) })
30+
}
31+
32+
public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?], Context) throws -> Any?) {
2933
filters[name] = .arguments(filter)
3034
}
3135
}
@@ -59,28 +63,28 @@ class DefaultExtension: Extension {
5963
registerFilter("join", filter: joinFilter)
6064
registerFilter("split", filter: splitFilter)
6165
registerFilter("indent", filter: indentFilter)
66+
registerFilter("filter", filter: filterFilter)
6267
}
6368
}
6469

6570

6671
protocol FilterType {
67-
func invoke(value: Any?, arguments: [Any?]) throws -> Any?
72+
func invoke(value: Any?, arguments: [Any?], context: Context) throws -> Any?
6873
}
6974

7075
enum Filter: FilterType {
7176
case simple(((Any?) throws -> Any?))
72-
case arguments(((Any?, [Any?]) throws -> Any?))
77+
case arguments(((Any?, [Any?], Context) throws -> Any?))
7378

74-
func invoke(value: Any?, arguments: [Any?]) throws -> Any? {
79+
func invoke(value: Any?, arguments: [Any?], context: Context) throws -> Any? {
7580
switch self {
7681
case let .simple(filter):
7782
if !arguments.isEmpty {
7883
throw TemplateSyntaxError("cannot invoke filter with an argument")
7984
}
80-
8185
return try filter(value)
8286
case let .arguments(filter):
83-
return try filter(value, arguments)
87+
return try filter(value, arguments, context)
8488
}
8589
}
8690
}

Sources/Filters.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func defaultFilter(value: Any?, arguments: [Any?]) -> Any? {
3939

4040
func joinFilter(value: Any?, arguments: [Any?]) throws -> Any? {
4141
guard arguments.count < 2 else {
42-
throw TemplateSyntaxError("'join' filter takes a single argument")
42+
throw TemplateSyntaxError("'join' filter takes at most one argument")
4343
}
4444

4545
let separator = stringify(arguments.first ?? "")
@@ -55,7 +55,7 @@ func joinFilter(value: Any?, arguments: [Any?]) throws -> Any? {
5555

5656
func splitFilter(value: Any?, arguments: [Any?]) throws -> Any? {
5757
guard arguments.count < 2 else {
58-
throw TemplateSyntaxError("'split' filter takes a single argument")
58+
throw TemplateSyntaxError("'split' filter takes at most one argument")
5959
}
6060

6161
let separator = stringify(arguments.first ?? " ")
@@ -111,3 +111,16 @@ func indent(_ content: String, indentation: String, indentFirst: Bool) -> String
111111
return result.joined(separator: "\n")
112112
}
113113

114+
func filterFilter(value: Any?, arguments: [Any?], context: Context) throws -> Any? {
115+
guard let value = value else { return nil }
116+
guard arguments.count == 1 else {
117+
throw TemplateSyntaxError("'filter' filter takes one argument")
118+
}
119+
120+
let attribute = stringify(arguments[0])
121+
122+
let expr = try context.environment.compileFilter("$0|\(attribute)")
123+
return try context.push(dictionary: ["$0": value]) {
124+
try expr.resolve(context)
125+
}
126+
}

Sources/ForTag.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class ForNode : NodeType {
4242
let resolvable = try parser.compileResolvable(components[3])
4343

4444
let `where` = hasToken("where", at: 4)
45-
? try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser)
45+
? try parser.compileExpression(components: Array(components.suffix(from: 5)))
4646
: nil
4747

4848
return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`)

Sources/IfTag.swift

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ final class IfExpressionParser {
100100
let tokens: [IfToken]
101101
var position: Int = 0
102102

103-
init(components: [String], tokenParser: TokenParser) throws {
103+
init(components: [String], environment: Environment) throws {
104104
self.tokens = try components.map { component in
105105
if let op = findOperator(name: component) {
106106
switch op {
@@ -111,7 +111,7 @@ final class IfExpressionParser {
111111
}
112112
}
113113

114-
return .variable(try tokenParser.compileResolvable(component))
114+
return .variable(try environment.compileResolvable(component))
115115
}
116116
}
117117

@@ -155,12 +155,6 @@ final class IfExpressionParser {
155155
}
156156

157157

158-
func parseExpression(components: [String], tokenParser: TokenParser) throws -> Expression {
159-
let parser = try IfExpressionParser(components: components, tokenParser: tokenParser)
160-
return try parser.parse()
161-
}
162-
163-
164158
/// Represents an if condition and the associated nodes when the condition
165159
/// evaluates
166160
final class IfCondition {
@@ -187,7 +181,7 @@ class IfNode : NodeType {
187181
var components = token.components()
188182
components.removeFirst()
189183

190-
let expression = try parseExpression(components: components, tokenParser: parser)
184+
let expression = try parser.compileExpression(components: components)
191185
let nodes = try parser.parse(until(["endif", "elif", "else"]))
192186
var conditions: [IfCondition] = [
193187
IfCondition(expression: expression, nodes: nodes)
@@ -197,7 +191,7 @@ class IfNode : NodeType {
197191
while let current = token, current.contents.hasPrefix("elif") {
198192
var components = current.components()
199193
components.removeFirst()
200-
let expression = try parseExpression(components: components, tokenParser: parser)
194+
let expression = try parser.compileExpression(components: components)
201195

202196
let nodes = try parser.parse(until(["endif", "elif", "else"]))
203197
token = parser.nextToken()
@@ -236,7 +230,7 @@ class IfNode : NodeType {
236230
_ = parser.nextToken()
237231
}
238232

239-
let expression = try parseExpression(components: components, tokenParser: parser)
233+
let expression = try parser.compileExpression(components: components)
240234
return IfNode(conditions: [
241235
IfCondition(expression: expression, nodes: trueNodes),
242236
IfCondition(expression: nil, nodes: falseNodes),

Sources/Parser.swift

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public class TokenParser {
4848
}
4949

5050
if let tag = token.components().first {
51-
let parser = try findTag(name: tag)
51+
let parser = try environment.findTag(name: tag)
5252
nodes.append(try parser(self, token))
5353
}
5454
case .comment:
@@ -71,8 +71,24 @@ public class TokenParser {
7171
tokens.insert(token, at: 0)
7272
}
7373

74+
public func compileFilter(_ token: String) throws -> Resolvable {
75+
return try environment.compileFilter(token)
76+
}
77+
78+
public func compileExpression(components: [String]) throws -> Expression {
79+
return try environment.compileExpression(components: components)
80+
}
81+
82+
public func compileResolvable(_ token: String) throws -> Resolvable {
83+
return try environment.compileResolvable(token)
84+
}
85+
86+
}
87+
88+
extension Environment {
89+
7490
func findTag(name: String) throws -> Extension.TagParser {
75-
for ext in environment.extensions {
91+
for ext in extensions {
7692
if let filter = ext.tags[name] {
7793
return filter
7894
}
@@ -82,7 +98,7 @@ public class TokenParser {
8298
}
8399

84100
func findFilter(_ name: String) throws -> FilterType {
85-
for ext in environment.extensions {
101+
for ext in extensions {
86102
if let filter = ext.filters[name] {
87103
return filter
88104
}
@@ -97,7 +113,7 @@ public class TokenParser {
97113
}
98114

99115
private func suggestedFilters(for name: String) -> [String] {
100-
let allFilters = environment.extensions.flatMap({ $0.filters.keys })
116+
let allFilters = extensions.flatMap({ $0.filters.keys })
101117

102118
let filtersWithDistance = allFilters
103119
.map({ (filterName: $0, distance: $0.levenshteinDistance(name)) })
@@ -111,11 +127,15 @@ public class TokenParser {
111127
}
112128

113129
public func compileFilter(_ token: String) throws -> Resolvable {
114-
return try FilterExpression(token: token, parser: self)
130+
return try FilterExpression(token: token, environment: self)
131+
}
132+
133+
public func compileExpression(components: [String]) throws -> Expression {
134+
return try IfExpressionParser(components: components, environment: self).parse()
115135
}
116136

117137
public func compileResolvable(_ token: String) throws -> Resolvable {
118-
return try RangeVariable(token, parser: self)
138+
return try RangeVariable(token, environment: self)
119139
?? compileFilter(token)
120140
}
121141

Sources/Variable.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ class FilterExpression : Resolvable {
88
let filters: [(FilterType, [Variable])]
99
let variable: Variable
1010

11-
init(token: String, parser: TokenParser) throws {
12-
let bits = token.characters.split(separator: "|").map({ String($0).trim(character: " ") })
11+
init(token: String, environment: Environment) throws {
12+
let bits = token.smartSplit(separator: "|").map({ String($0).trim(character: " ") })
1313
if bits.isEmpty {
1414
filters = []
1515
variable = Variable("")
@@ -22,7 +22,7 @@ class FilterExpression : Resolvable {
2222
do {
2323
filters = try filterBits.map {
2424
let (name, arguments) = parseFilterComponents(token: $0)
25-
let filter = try parser.findFilter(name)
25+
let filter = try environment.findFilter(name)
2626
return (filter, arguments)
2727
}
2828
} catch {
@@ -36,7 +36,7 @@ class FilterExpression : Resolvable {
3636

3737
return try filters.reduce(result) { x, y in
3838
let arguments = try y.1.map { try $0.resolve(context) }
39-
return try y.0.invoke(value: x, arguments: arguments)
39+
return try y.0.invoke(value: x, arguments: arguments, context: context)
4040
}
4141
}
4242
}
@@ -138,14 +138,14 @@ public struct RangeVariable: Resolvable {
138138
public let from: Resolvable
139139
public let to: Resolvable
140140

141-
public init?(_ token: String, parser: TokenParser) throws {
141+
public init?(_ token: String, environment: Environment) throws {
142142
let components = token.components(separatedBy: "...")
143143
guard components.count == 2 else {
144144
return nil
145145
}
146146

147-
self.from = try parser.compileFilter(components[0])
148-
self.to = try parser.compileFilter(components[1])
147+
self.from = try environment.compileFilter(components[0])
148+
self.to = try environment.compileFilter(components[1])
149149
}
150150

151151
public func resolve(_ context: Context) throws -> Any? {

Tests/StencilTests/ExpressionSpec.swift

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -105,19 +105,19 @@ func testExpressions() {
105105

106106
$0.describe("expression parsing") {
107107
$0.it("can parse a variable expression") {
108-
let expression = try parseExpression(components: ["value"], tokenParser: parser)
108+
let expression = try parser.compileExpression(components: ["value"])
109109
try expect(expression.evaluate(context: Context())).to.beFalse()
110110
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue()
111111
}
112112

113113
$0.it("can parse a not expression") {
114-
let expression = try parseExpression(components: ["not", "value"], tokenParser: parser)
114+
let expression = try parser.compileExpression(components: ["not", "value"])
115115
try expect(expression.evaluate(context: Context())).to.beTrue()
116116
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse()
117117
}
118118

119119
$0.describe("and expression") {
120-
let expression = try! parseExpression(components: ["lhs", "and", "rhs"], tokenParser: parser)
120+
let expression = try! parser.compileExpression(components: ["lhs", "and", "rhs"])
121121

122122
$0.it("evaluates to false with lhs false") {
123123
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse()
@@ -137,7 +137,7 @@ func testExpressions() {
137137
}
138138

139139
$0.describe("or expression") {
140-
let expression = try! parseExpression(components: ["lhs", "or", "rhs"], tokenParser: parser)
140+
let expression = try! parser.compileExpression(components: ["lhs", "or", "rhs"])
141141

142142
$0.it("evaluates to true with lhs true") {
143143
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue()
@@ -157,7 +157,7 @@ func testExpressions() {
157157
}
158158

159159
$0.describe("equality expression") {
160-
let expression = try! parseExpression(components: ["lhs", "==", "rhs"], tokenParser: parser)
160+
let expression = try! parser.compileExpression(components: ["lhs", "==", "rhs"])
161161

162162
$0.it("evaluates to true with equal lhs/rhs") {
163163
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue()
@@ -193,7 +193,7 @@ func testExpressions() {
193193
}
194194

195195
$0.describe("inequality expression") {
196-
let expression = try! parseExpression(components: ["lhs", "!=", "rhs"], tokenParser: parser)
196+
let expression = try! parser.compileExpression(components: ["lhs", "!=", "rhs"])
197197

198198
$0.it("evaluates to true with inequal lhs/rhs") {
199199
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue()
@@ -205,7 +205,7 @@ func testExpressions() {
205205
}
206206

207207
$0.describe("more than expression") {
208-
let expression = try! parseExpression(components: ["lhs", ">", "rhs"], tokenParser: parser)
208+
let expression = try! parser.compileExpression(components: ["lhs", ">", "rhs"])
209209

210210
$0.it("evaluates to true with lhs > rhs") {
211211
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue()
@@ -217,7 +217,7 @@ func testExpressions() {
217217
}
218218

219219
$0.describe("more than equal expression") {
220-
let expression = try! parseExpression(components: ["lhs", ">=", "rhs"], tokenParser: parser)
220+
let expression = try! parser.compileExpression(components: ["lhs", ">=", "rhs"])
221221

222222
$0.it("evaluates to true with lhs == rhs") {
223223
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
@@ -229,7 +229,7 @@ func testExpressions() {
229229
}
230230

231231
$0.describe("less than expression") {
232-
let expression = try! parseExpression(components: ["lhs", "<", "rhs"], tokenParser: parser)
232+
let expression = try! parser.compileExpression(components: ["lhs", "<", "rhs"])
233233

234234
$0.it("evaluates to true with lhs < rhs") {
235235
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue()
@@ -241,7 +241,7 @@ func testExpressions() {
241241
}
242242

243243
$0.describe("less than equal expression") {
244-
let expression = try! parseExpression(components: ["lhs", "<=", "rhs"], tokenParser: parser)
244+
let expression = try! parser.compileExpression(components: ["lhs", "<=", "rhs"])
245245

246246
$0.it("evaluates to true with lhs == rhs") {
247247
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
@@ -253,7 +253,7 @@ func testExpressions() {
253253
}
254254

255255
$0.describe("multiple expression") {
256-
let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"], tokenParser: parser)
256+
let expression = try! parser.compileExpression(components: ["one", "or", "two", "and", "not", "three"])
257257

258258
$0.it("evaluates to true with one") {
259259
try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue()
@@ -281,7 +281,7 @@ func testExpressions() {
281281
}
282282

283283
$0.describe("in expression") {
284-
let expression = try! parseExpression(components: ["lhs", "in", "rhs"], tokenParser: parser)
284+
let expression = try! parser.compileExpression(components: ["lhs", "in", "rhs"])
285285

286286
$0.it("evaluates to true when rhs contains lhs") {
287287
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]]))).to.beTrue()

0 commit comments

Comments
 (0)