From 60b378d482011b41bc630f85a8b3f931a55e434d Mon Sep 17 00:00:00 2001 From: Kyle Fuller Date: Sun, 27 Nov 2016 01:59:57 +0000 Subject: [PATCH] feat(filters): Allow filters with arguments --- CHANGELOG.md | 4 ++++ README.md | 22 +++++++++++++++++++- Sources/Namespace.swift | 32 +++++++++++++++++++++++++++-- Sources/Parser.swift | 3 +-- Sources/Variable.swift | 18 +++++++++++++--- Tests/StencilTests/FilterSpec.swift | 26 +++++++++++++++++++++-- 6 files changed, 95 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2230d4..6c9863f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Master +### Enhancements + +- You may now register custom template filters which make use of arguments. + ### Bug Fixes - Variables (`{{ variable.5 }}`) that reference an array index at an unknown diff --git a/README.md b/README.md index 7d87fdd..b28913c 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,27 @@ let rendered = try template.render(context, namespace: namespace) #### Registering custom filters ```swift -namespace.registerFilter("double") { value in +namespace.registerFilter("double") { (value: Any?) in + if let value = value as? Int { + return value * 2 + } + + return value +} +``` + +#### Registering custom filters with arguments + +```swift +namespace.registerFilter("multiply") { (value: Any?, arguments: [Any?]) in + let amount: Int + + if let value = arguments.first as? Int { + amount = value + } else { + throw TemplateSyntaxError("multiple tag must be called with an integer argument") + } + if let value = value as? Int { return value * 2 } diff --git a/Sources/Namespace.swift b/Sources/Namespace.swift index b7f6926..a6187d3 100644 --- a/Sources/Namespace.swift +++ b/Sources/Namespace.swift @@ -1,3 +1,26 @@ +public protocol FilterType { + func invoke(value: Any?, arguments: [Any?]) throws -> Any? +} + +enum Filter: FilterType { + case simple(((Any?) throws -> Any?)) + case arguments(((Any?, [Any?]) throws -> Any?)) + + func invoke(value: Any?, arguments: [Any?]) throws -> Any? { + switch self { + case let .simple(filter): + if !arguments.isEmpty { + throw TemplateSyntaxError("cannot invoke filter with an argument") + } + + return try filter(value) + case let .arguments(filter): + return try filter(value, arguments) + } + } +} + + open class Namespace { public typealias TagParser = (TokenParser, Token) throws -> NodeType @@ -40,7 +63,12 @@ open class Namespace { } /// Registers a template filter with the given name - open func registerFilter(_ name: String, filter: @escaping Filter) { - filters[name] = filter + open func registerFilter(_ name: String, filter: @escaping (Any?) throws -> Any?) { + filters[name] = .simple(filter) + } + + /// Registers a template filter with the given name + open func registerFilter(_ name: String, filter: @escaping (Any?, [Any?]) throws -> Any?) { + filters[name] = .arguments(filter) } } diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 864b304..a91ca31 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -12,7 +12,6 @@ public func until(_ tags: [String]) -> ((TokenParser, Token) -> Bool) { } } -public typealias Filter = (Any?) throws -> Any? /// A class for parsing an array of tokens and converts them into a collection of Node's open class TokenParser { @@ -77,7 +76,7 @@ open class TokenParser { tokens.insert(token, at: 0) } - open func findFilter(_ name: String) throws -> Filter { + open func findFilter(_ name: String) throws -> FilterType { if let filter = namespace.filters[name] { return filter } diff --git a/Sources/Variable.swift b/Sources/Variable.swift index 8a523d7..0d03870 100644 --- a/Sources/Variable.swift +++ b/Sources/Variable.swift @@ -2,7 +2,7 @@ import Foundation class FilterExpression : Resolvable { - let filters: [Filter] + let filters: [(FilterType, [Variable])] let variable: Variable init(token: String, parser: TokenParser) throws { @@ -17,7 +17,11 @@ class FilterExpression : Resolvable { let filterBits = bits[bits.indices.suffix(from: 1)] do { - filters = try filterBits.map { try parser.findFilter($0) } + filters = try filterBits.map { + let (name, arguments) = parseFilterComponents(token: $0) + let filter = try parser.findFilter(name) + return (filter, arguments) + } } catch { filters = [] throw error @@ -28,7 +32,8 @@ class FilterExpression : Resolvable { let result = try variable.resolve(context) return try filters.reduce(result) { x, y in - return try y(x) + let arguments = try y.1.map { try $0.resolve(context) } + return try y.0.invoke(value: x, arguments: arguments) } } } @@ -135,3 +140,10 @@ extension Dictionary : Normalizable { return dictionary } } + +func parseFilterComponents(token: String) -> (String, [Variable]) { + var components = token.characters.split(separator: ":").map(String.init) + let name = components.removeFirst() + let variables = components.joined(separator: ":").characters.split(separator: ",").map { Variable(String($0)) } + return (name, variables) +} diff --git a/Tests/StencilTests/FilterSpec.swift b/Tests/StencilTests/FilterSpec.swift index cef617f..b88e99f 100644 --- a/Tests/StencilTests/FilterSpec.swift +++ b/Tests/StencilTests/FilterSpec.swift @@ -10,7 +10,7 @@ func testFilter() { let template = Template(templateString: "{{ name|repeat }}") let namespace = Namespace() - namespace.registerFilter("repeat") { value in + namespace.registerFilter("repeat") { (value: Any?) in if let value = value as? String { return "\(value) \(value)" } @@ -22,10 +22,27 @@ func testFilter() { try expect(result) == "Kyle Kyle" } + $0.it("allows you to register a custom filter which accepts arguments") { + let template = Template(templateString: "{{ name|repeat:'value' }}") + + let namespace = Namespace() + namespace.registerFilter("repeat") { value, arguments in + print(arguments) + if !arguments.isEmpty { + return "\(value!) \(value!) with args \(arguments.first!!)" + } + + return nil + } + + let result = try template.render(Context(dictionary: context, namespace: namespace)) + try expect(result) == "Kyle Kyle with args value" + } + $0.it("allows you to register a custom which throws") { let template = Template(templateString: "{{ name|repeat }}") let namespace = Namespace() - namespace.registerFilter("repeat") { value in + namespace.registerFilter("repeat") { (value: Any?) in throw TemplateSyntaxError("No Repeat") } @@ -37,6 +54,11 @@ func testFilter() { let result = try template.render(Context(dictionary: ["name": "kyle"])) try expect(result) == "KYLE" } + + $0.it("throws when you pass arguments to simple filter") { + let template = Template(templateString: "{{ name|uppercase:5 }}") + try expect(try template.render(Context(dictionary: ["name": "kyle"]))).toThrow() + } }