From 564ccb7af7de61765c3a83f8a59c6bbf40efadd7 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Thu, 22 Mar 2018 23:14:30 +0000 Subject: [PATCH 1/6] added filter to apply dynamic filter --- CHANGELOG.md | 2 + Sources/Expression.swift | 2 +- Sources/Extension.swift | 14 +++-- Sources/Filters.swift | 17 ++++++- Sources/ForTag.swift | 2 +- Sources/IfTag.swift | 16 ++---- Sources/Parser.swift | 32 +++++++++--- Sources/Variable.swift | 14 ++--- Tests/StencilTests/ExpressionSpec.swift | 24 ++++----- Tests/StencilTests/FilterSpec.swift | 68 +++++++++++++++++-------- Tests/StencilTests/ForNodeSpec.swift | 6 ++- Tests/StencilTests/VariableSpec.swift | 2 +- 12 files changed, 129 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f009cbb..b1ae7381 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ [Ilya Puchka](https://github.com/yonaskolb) [#178](https://github.com/stencilproject/Stencil/pull/178) +- Added support for dynamic filter using `filter` filter + ### Bug Fixes - Fixed using quote as a filter parameter diff --git a/Sources/Expression.swift b/Sources/Expression.swift index c7199fc6..bb4ef673 100644 --- a/Sources/Expression.swift +++ b/Sources/Expression.swift @@ -1,4 +1,4 @@ -protocol Expression: CustomStringConvertible { +public protocol Expression: CustomStringConvertible { func evaluate(context: Context) throws -> Bool } diff --git a/Sources/Extension.swift b/Sources/Extension.swift index 33a9925a..0bb826dd 100644 --- a/Sources/Extension.swift +++ b/Sources/Extension.swift @@ -26,6 +26,10 @@ open class Extension { /// Registers a template filter with the given name public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?]) throws -> Any?) { + filters[name] = .arguments({ value, args, _ in try filter(value, args) }) + } + + public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?], Context) throws -> Any?) { filters[name] = .arguments(filter) } } @@ -59,28 +63,28 @@ class DefaultExtension: Extension { registerFilter("join", filter: joinFilter) registerFilter("split", filter: splitFilter) registerFilter("indent", filter: indentFilter) + registerFilter("filter", filter: filterFilter) } } protocol FilterType { - func invoke(value: Any?, arguments: [Any?]) throws -> Any? + func invoke(value: Any?, arguments: [Any?], context: Context) throws -> Any? } enum Filter: FilterType { case simple(((Any?) throws -> Any?)) - case arguments(((Any?, [Any?]) throws -> Any?)) + case arguments(((Any?, [Any?], Context) throws -> Any?)) - func invoke(value: Any?, arguments: [Any?]) throws -> Any? { + func invoke(value: Any?, arguments: [Any?], context: Context) 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) + return try filter(value, arguments, context) } } } diff --git a/Sources/Filters.swift b/Sources/Filters.swift index fece6ebd..4d664f31 100644 --- a/Sources/Filters.swift +++ b/Sources/Filters.swift @@ -39,7 +39,7 @@ func defaultFilter(value: Any?, arguments: [Any?]) -> Any? { func joinFilter(value: Any?, arguments: [Any?]) throws -> Any? { guard arguments.count < 2 else { - throw TemplateSyntaxError("'join' filter takes a single argument") + throw TemplateSyntaxError("'join' filter takes at most one argument") } let separator = stringify(arguments.first ?? "") @@ -55,7 +55,7 @@ func joinFilter(value: Any?, arguments: [Any?]) throws -> Any? { func splitFilter(value: Any?, arguments: [Any?]) throws -> Any? { guard arguments.count < 2 else { - throw TemplateSyntaxError("'split' filter takes a single argument") + throw TemplateSyntaxError("'split' filter takes at most one argument") } let separator = stringify(arguments.first ?? " ") @@ -111,3 +111,16 @@ func indent(_ content: String, indentation: String, indentFirst: Bool) -> String return result.joined(separator: "\n") } +func filterFilter(value: Any?, arguments: [Any?], context: Context) throws -> Any? { + guard let value = value else { return nil } + guard arguments.count == 1 else { + throw TemplateSyntaxError("'filter' filter takes one argument") + } + + let attribute = stringify(arguments[0]) + + let expr = try context.environment.compileFilter("$0|\(attribute)") + return try context.push(dictionary: ["$0": value]) { + try expr.resolve(context) + } +} diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index 4d7eff27..2450d5f9 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -42,7 +42,7 @@ class ForNode : NodeType { let resolvable = try parser.compileResolvable(components[3]) let `where` = hasToken("where", at: 4) - ? try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser) + ? try parser.compileExpression(components: Array(components.suffix(from: 5))) : nil return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`) diff --git a/Sources/IfTag.swift b/Sources/IfTag.swift index a857d3eb..e8fddad0 100644 --- a/Sources/IfTag.swift +++ b/Sources/IfTag.swift @@ -100,7 +100,7 @@ final class IfExpressionParser { let tokens: [IfToken] var position: Int = 0 - init(components: [String], tokenParser: TokenParser) throws { + init(components: [String], environment: Environment) throws { self.tokens = try components.map { component in if let op = findOperator(name: component) { switch op { @@ -111,7 +111,7 @@ final class IfExpressionParser { } } - return .variable(try tokenParser.compileResolvable(component)) + return .variable(try environment.compileResolvable(component)) } } @@ -155,12 +155,6 @@ final class IfExpressionParser { } -func parseExpression(components: [String], tokenParser: TokenParser) throws -> Expression { - let parser = try IfExpressionParser(components: components, tokenParser: tokenParser) - return try parser.parse() -} - - /// Represents an if condition and the associated nodes when the condition /// evaluates final class IfCondition { @@ -187,7 +181,7 @@ class IfNode : NodeType { var components = token.components() components.removeFirst() - let expression = try parseExpression(components: components, tokenParser: parser) + let expression = try parser.compileExpression(components: components) let nodes = try parser.parse(until(["endif", "elif", "else"])) var conditions: [IfCondition] = [ IfCondition(expression: expression, nodes: nodes) @@ -197,7 +191,7 @@ class IfNode : NodeType { while let current = token, current.contents.hasPrefix("elif") { var components = current.components() components.removeFirst() - let expression = try parseExpression(components: components, tokenParser: parser) + let expression = try parser.compileExpression(components: components) let nodes = try parser.parse(until(["endif", "elif", "else"])) token = parser.nextToken() @@ -236,7 +230,7 @@ class IfNode : NodeType { _ = parser.nextToken() } - let expression = try parseExpression(components: components, tokenParser: parser) + let expression = try parser.compileExpression(components: components) return IfNode(conditions: [ IfCondition(expression: expression, nodes: trueNodes), IfCondition(expression: nil, nodes: falseNodes), diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 81a44e10..7bd7ad73 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -48,7 +48,7 @@ public class TokenParser { } if let tag = token.components().first { - let parser = try findTag(name: tag) + let parser = try environment.findTag(name: tag) nodes.append(try parser(self, token)) } case .comment: @@ -71,8 +71,24 @@ public class TokenParser { tokens.insert(token, at: 0) } + public func compileFilter(_ token: String) throws -> Resolvable { + return try environment.compileFilter(token) + } + + public func compileExpression(components: [String]) throws -> Expression { + return try environment.compileExpression(components: components) + } + + public func compileResolvable(_ token: String) throws -> Resolvable { + return try environment.compileResolvable(token) + } + +} + +extension Environment { + func findTag(name: String) throws -> Extension.TagParser { - for ext in environment.extensions { + for ext in extensions { if let filter = ext.tags[name] { return filter } @@ -82,7 +98,7 @@ public class TokenParser { } func findFilter(_ name: String) throws -> FilterType { - for ext in environment.extensions { + for ext in extensions { if let filter = ext.filters[name] { return filter } @@ -97,7 +113,7 @@ public class TokenParser { } private func suggestedFilters(for name: String) -> [String] { - let allFilters = environment.extensions.flatMap({ $0.filters.keys }) + let allFilters = extensions.flatMap({ $0.filters.keys }) let filtersWithDistance = allFilters .map({ (filterName: $0, distance: $0.levenshteinDistance(name)) }) @@ -111,11 +127,15 @@ public class TokenParser { } public func compileFilter(_ token: String) throws -> Resolvable { - return try FilterExpression(token: token, parser: self) + return try FilterExpression(token: token, environment: self) + } + + public func compileExpression(components: [String]) throws -> Expression { + return try IfExpressionParser(components: components, environment: self).parse() } public func compileResolvable(_ token: String) throws -> Resolvable { - return try RangeVariable(token, parser: self) + return try RangeVariable(token, environment: self) ?? compileFilter(token) } diff --git a/Sources/Variable.swift b/Sources/Variable.swift index b3570219..ac0816ef 100644 --- a/Sources/Variable.swift +++ b/Sources/Variable.swift @@ -8,8 +8,8 @@ class FilterExpression : Resolvable { let filters: [(FilterType, [Variable])] let variable: Variable - init(token: String, parser: TokenParser) throws { - let bits = token.characters.split(separator: "|").map({ String($0).trim(character: " ") }) + init(token: String, environment: Environment) throws { + let bits = token.smartSplit(separator: "|").map({ String($0).trim(character: " ") }) if bits.isEmpty { filters = [] variable = Variable("") @@ -22,7 +22,7 @@ class FilterExpression : Resolvable { do { filters = try filterBits.map { let (name, arguments) = parseFilterComponents(token: $0) - let filter = try parser.findFilter(name) + let filter = try environment.findFilter(name) return (filter, arguments) } } catch { @@ -36,7 +36,7 @@ class FilterExpression : Resolvable { return try filters.reduce(result) { x, y in let arguments = try y.1.map { try $0.resolve(context) } - return try y.0.invoke(value: x, arguments: arguments) + return try y.0.invoke(value: x, arguments: arguments, context: context) } } } @@ -138,14 +138,14 @@ public struct RangeVariable: Resolvable { public let from: Resolvable public let to: Resolvable - public init?(_ token: String, parser: TokenParser) throws { + public init?(_ token: String, environment: Environment) throws { let components = token.components(separatedBy: "...") guard components.count == 2 else { return nil } - self.from = try parser.compileFilter(components[0]) - self.to = try parser.compileFilter(components[1]) + self.from = try environment.compileFilter(components[0]) + self.to = try environment.compileFilter(components[1]) } public func resolve(_ context: Context) throws -> Any? { diff --git a/Tests/StencilTests/ExpressionSpec.swift b/Tests/StencilTests/ExpressionSpec.swift index c41575f4..b214a780 100644 --- a/Tests/StencilTests/ExpressionSpec.swift +++ b/Tests/StencilTests/ExpressionSpec.swift @@ -105,19 +105,19 @@ func testExpressions() { $0.describe("expression parsing") { $0.it("can parse a variable expression") { - let expression = try parseExpression(components: ["value"], tokenParser: parser) + let expression = try parser.compileExpression(components: ["value"]) try expect(expression.evaluate(context: Context())).to.beFalse() try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue() } $0.it("can parse a not expression") { - let expression = try parseExpression(components: ["not", "value"], tokenParser: parser) + let expression = try parser.compileExpression(components: ["not", "value"]) try expect(expression.evaluate(context: Context())).to.beTrue() try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse() } $0.describe("and expression") { - let expression = try! parseExpression(components: ["lhs", "and", "rhs"], tokenParser: parser) + let expression = try! parser.compileExpression(components: ["lhs", "and", "rhs"]) $0.it("evaluates to false with lhs false") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse() @@ -137,7 +137,7 @@ func testExpressions() { } $0.describe("or expression") { - let expression = try! parseExpression(components: ["lhs", "or", "rhs"], tokenParser: parser) + let expression = try! parser.compileExpression(components: ["lhs", "or", "rhs"]) $0.it("evaluates to true with lhs true") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue() @@ -157,7 +157,7 @@ func testExpressions() { } $0.describe("equality expression") { - let expression = try! parseExpression(components: ["lhs", "==", "rhs"], tokenParser: parser) + let expression = try! parser.compileExpression(components: ["lhs", "==", "rhs"]) $0.it("evaluates to true with equal lhs/rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue() @@ -193,7 +193,7 @@ func testExpressions() { } $0.describe("inequality expression") { - let expression = try! parseExpression(components: ["lhs", "!=", "rhs"], tokenParser: parser) + let expression = try! parser.compileExpression(components: ["lhs", "!=", "rhs"]) $0.it("evaluates to true with inequal lhs/rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue() @@ -205,7 +205,7 @@ func testExpressions() { } $0.describe("more than expression") { - let expression = try! parseExpression(components: ["lhs", ">", "rhs"], tokenParser: parser) + let expression = try! parser.compileExpression(components: ["lhs", ">", "rhs"]) $0.it("evaluates to true with lhs > rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue() @@ -217,7 +217,7 @@ func testExpressions() { } $0.describe("more than equal expression") { - let expression = try! parseExpression(components: ["lhs", ">=", "rhs"], tokenParser: parser) + let expression = try! parser.compileExpression(components: ["lhs", ">=", "rhs"]) $0.it("evaluates to true with lhs == rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() @@ -229,7 +229,7 @@ func testExpressions() { } $0.describe("less than expression") { - let expression = try! parseExpression(components: ["lhs", "<", "rhs"], tokenParser: parser) + let expression = try! parser.compileExpression(components: ["lhs", "<", "rhs"]) $0.it("evaluates to true with lhs < rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue() @@ -241,7 +241,7 @@ func testExpressions() { } $0.describe("less than equal expression") { - let expression = try! parseExpression(components: ["lhs", "<=", "rhs"], tokenParser: parser) + let expression = try! parser.compileExpression(components: ["lhs", "<=", "rhs"]) $0.it("evaluates to true with lhs == rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() @@ -253,7 +253,7 @@ func testExpressions() { } $0.describe("multiple expression") { - let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"], tokenParser: parser) + let expression = try! parser.compileExpression(components: ["one", "or", "two", "and", "not", "three"]) $0.it("evaluates to true with one") { try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue() @@ -281,7 +281,7 @@ func testExpressions() { } $0.describe("in expression") { - let expression = try! parseExpression(components: ["lhs", "in", "rhs"], tokenParser: parser) + let expression = try! parser.compileExpression(components: ["lhs", "in", "rhs"]) $0.it("evaluates to true when rhs contains lhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]]))).to.beTrue() diff --git a/Tests/StencilTests/FilterSpec.swift b/Tests/StencilTests/FilterSpec.swift index f8de1b87..8b3aba7c 100644 --- a/Tests/StencilTests/FilterSpec.swift +++ b/Tests/StencilTests/FilterSpec.swift @@ -3,14 +3,19 @@ import Spectre func testFilter() { + + func environmentWithFilter(_ name: String, closure: @escaping (Any?) throws -> Any?) -> Environment { + let filterExtension = Extension() + filterExtension.registerFilter(name, filter: closure) + return Environment(extensions: [filterExtension]) + } + describe("template filters") { let context: [String: Any] = ["name": "Kyle"] $0.it("allows you to register a custom filter") { let template = Template(templateString: "{{ name|repeat }}") - - let repeatExtension = Extension() - repeatExtension.registerFilter("repeat") { (value: Any?) in + let env = environmentWithFilter("repeat") { (value: Any?) in if let value = value as? String { return "\(value) \(value)" } @@ -18,7 +23,7 @@ func testFilter() { return nil } - let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))) + let result = try template.render(Context(dictionary: context, environment: env)) try expect(result) == "Kyle Kyle" } @@ -39,19 +44,19 @@ func testFilter() { } $0.it("allows you to register a custom filter which accepts several arguments") { - let template = Template(templateString: "{{ name|repeat:'value\"1\"',\"value'2'\",'(key, value)' }}") - - let repeatExtension = Extension() - repeatExtension.registerFilter("repeat") { value, arguments in - if !arguments.isEmpty { - return "\(value!) \(value!) with args 0: \(arguments[0]!), 1: \(arguments[1]!), 2: \(arguments[2]!)" - } - - return nil + let template = Template(templateString: "{{ name|repeat:'value\"1\"',\"value'2'\",'(key, value)' }}") + + let repeatExtension = Extension() + repeatExtension.registerFilter("repeat") { value, arguments in + if !arguments.isEmpty { + return "\(value!) \(value!) with args 0: \(arguments[0]!), 1: \(arguments[1]!), 2: \(arguments[2]!)" } - - let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))) - try expect(result) == "Kyle Kyle with args 0: value\"1\", 1: value'2', 2: (key, value)" + + return nil + } + + let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))) + try expect(result) == "Kyle Kyle with args 0: value\"1\", 1: value'2', 2: (key, value)" } $0.it("allows you to register a custom which throws") { @@ -67,13 +72,11 @@ func testFilter() { $0.it("allows you to override a default filter") { let template = Template(templateString: "{{ name|join }}") - - let repeatExtension = Extension() - repeatExtension.registerFilter("join") { (value: Any?) in + let env = environmentWithFilter("join") { (value: Any?) in return "joined" } - let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))) + let result = try template.render(Context(dictionary: context, environment: env)) try expect(result) == "joined" } @@ -212,7 +215,6 @@ func testFilter() { } } - describe("filter suggestion") { $0.it("made for unknown filter") { @@ -244,7 +246,6 @@ func testFilter() { } - describe("indent filter") { $0.it("indents content") { let template = Template(templateString: "{{ value|indent:2 }}") @@ -270,4 +271,27 @@ func testFilter() { try expect(result) == "One\n\n\n Two\n\n" } } + + describe("dynamic filter") { + + $0.it("can apply dynamic filter") { + let template = Template(templateString: "{{ name|filter:somefilter }}") + let result = try template.render(Context(dictionary: ["name": "Jhon", "somefilter": "uppercase"])) + try expect(result) == "JHON" + } + + $0.it("can apply dynamic filter on array") { + let template = Template(templateString: "{{ values|filter:joinfilter }}") + let result = try template.render(Context(dictionary: ["values": [1, 2, 3], "joinfilter": "join:\", \""])) + try expect(result) == "1, 2, 3" + } + + $0.it("throws on unknown dynamic filter") { + let template = Template(templateString: "{{ values|filter:unknown }}") + let context = Context(dictionary: ["values": [1, 2, 3], "unknown": "absurd"]) + try expect(try template.render(context)).toThrow() + } + + } + } diff --git a/Tests/StencilTests/ForNodeSpec.swift b/Tests/StencilTests/ForNodeSpec.swift index 4bb3fca4..03b40fc6 100644 --- a/Tests/StencilTests/ForNodeSpec.swift +++ b/Tests/StencilTests/ForNodeSpec.swift @@ -98,7 +98,8 @@ func testForNode() { $0.it("renders the given nodes while filtering items using where expression") { let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")] - let `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment())) + let parser = TokenParser(tokens: [], environment: Environment()) + let `where` = try parser.compileExpression(components: ["item", ">", "1"]) let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [], where: `where`) try expect(try node.render(context)) == "2132" } @@ -106,7 +107,8 @@ func testForNode() { $0.it("renders the given empty nodes when all items filtered out with where expression") { let nodes: [NodeType] = [VariableNode(variable: "item")] let emptyNodes: [NodeType] = [TextNode(text: "empty")] - let `where` = try parseExpression(components: ["item", "==", "0"], tokenParser: TokenParser(tokens: [], environment: Environment())) + let parser = TokenParser(tokens: [], environment: Environment()) + let `where` = try parser.compileExpression(components: ["item", "==", "0"]) let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes, where: `where`) try expect(try node.render(context)) == "empty" } diff --git a/Tests/StencilTests/VariableSpec.swift b/Tests/StencilTests/VariableSpec.swift index 3ca28cbc..3e1b634b 100644 --- a/Tests/StencilTests/VariableSpec.swift +++ b/Tests/StencilTests/VariableSpec.swift @@ -200,7 +200,7 @@ func testVariable() { }() func makeVariable(_ token: String) throws -> RangeVariable? { - return try RangeVariable(token, parser: TokenParser(tokens: [], environment: context.environment)) + return try RangeVariable(token, environment: context.environment) } $0.it("can resolve closed range as array") { From f3d5843e7841a3df20530cbb3c659e6c15df015e Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 4 Aug 2018 19:49:57 +0100 Subject: [PATCH 2/6] updated CHANGELOG --- CHANGELOG.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1ae7381..f688398b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,16 +8,18 @@ [Yonas Kolb](https://github.com/yonaskolb) [#394](https://github.com/stencilproject/Stencil/pull/214) -- Adds support for using spaces in filter expression - [Ilya Puchka](https://github.com/yonaskolb) +- Adds support for using spaces in filter expression. + [Ilya Puchka](https://github.com/ilyapuchka) [#178](https://github.com/stencilproject/Stencil/pull/178) -- Added support for dynamic filter using `filter` filter +- Added support for dynamic filter using `filter` filter. + [Ilya Puchka](https://github.com/ilyapuchka) + [#203](https://github.com/stencilproject/Stencil/pull/203) ### Bug Fixes - Fixed using quote as a filter parameter - [Ilya Puchka](https://github.com/yonaskolb) + [Ilya Puchka](https://github.com/ilyapuchka) [#210](https://github.com/stencilproject/Stencil/pull/210) From e795f052eaf0bb54a725dbee8a836d1c40effa3c Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 4 Aug 2018 20:19:50 +0100 Subject: [PATCH 3/6] updated docs --- docs/builtins.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/builtins.rst b/docs/builtins.rst index d4cc99a3..de23ac95 100644 --- a/docs/builtins.rst +++ b/docs/builtins.rst @@ -373,3 +373,13 @@ Filter accepts several arguments: * indentation character: character to be used for indentation. Default is a space. * indent first line: whether first line of output should be indented or not. Default is ``false``. +``filter`` +~~~~~~~~~ + +Applies filter by its name, provided as an argument. + +.. code-block:: html+django + + {{ string|filter:myfilter }} + +This expression will resolve `myfilter` variable and will apply filter with such name to `string` variable. From dbb5e14e9f04e3bd5bc287012379f608c879f15e Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 1 Oct 2018 21:59:03 +0100 Subject: [PATCH 4/6] solve merge conflict issues --- Sources/Extension.swift | 1 + Sources/ForTag.swift | 2 +- Sources/IfTag.swift | 20 ++++++------- Sources/Node.swift | 4 +-- Sources/Parser.swift | 36 +++++++++++----------- Sources/Variable.swift | 13 ++++---- Tests/StencilTests/ExpressionSpec.swift | 3 +- Tests/StencilTests/FilterSpec.swift | 40 ++++++++++++------------- Tests/StencilTests/ForNodeSpec.swift | 6 ++-- Tests/StencilTests/VariableSpec.swift | 3 +- 10 files changed, 63 insertions(+), 65 deletions(-) diff --git a/Sources/Extension.swift b/Sources/Extension.swift index 2aac1d93..5af819ee 100644 --- a/Sources/Extension.swift +++ b/Sources/Extension.swift @@ -38,6 +38,7 @@ open class Extension { filters[name] = .arguments({ value, args, _ in try filter(value, args) }) } + /// Registers a template filter with the given name public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?], Context) throws -> Any?) { filters[name] = .arguments(filter) } diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index 0bc8775c..3f2a753f 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -31,7 +31,7 @@ class ForNode : NodeType { let resolvable = try parser.compileResolvable(components[3], containedIn: token) let `where` = hasToken("where", at: 4) - ? try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser, token: token) + ? try parser.compileExpression(components: Array(components.suffix(from: 5)), token: token) : nil let forNodes = try parser.parse(until(["endfor", "empty"])) diff --git a/Sources/IfTag.swift b/Sources/IfTag.swift index 0b566225..16fd0ee1 100644 --- a/Sources/IfTag.swift +++ b/Sources/IfTag.swift @@ -111,11 +111,11 @@ final class IfExpressionParser { self.tokens = tokens } - static func parser(components: [String], tokenParser: TokenParser, token: Token) throws -> IfExpressionParser { - return try IfExpressionParser(components: ArraySlice(components), tokenParser: tokenParser, token: token) + static func parser(components: [String], environment: Environment, token: Token) throws -> IfExpressionParser { + return try IfExpressionParser(components: ArraySlice(components), environment: environment, token: token) } - private init(components: ArraySlice, tokenParser: TokenParser, token: Token) throws { + private init(components: ArraySlice, environment: Environment, token: Token) throws { var parsedComponents = Set() var bracketsBalance = 0 self.tokens = try zip(components.indices, components).compactMap { (index, component) in @@ -125,7 +125,7 @@ final class IfExpressionParser { bracketsBalance += 1 let (expression, parsedCount) = try IfExpressionParser.subExpression( from: components.suffix(from: index + 1), - tokenParser: tokenParser, + environment: environment, token: token ) parsedComponents.formUnion(Set(index...(index + parsedCount))) @@ -147,12 +147,12 @@ final class IfExpressionParser { return .prefix(name: name, bindingPower: bindingPower, operatorType: operatorType) } } - return .variable(try tokenParser.compileResolvable(component, containedIn: token)) + return .variable(try environment.compileResolvable(component, containedIn: token)) } } } - private static func subExpression(from components: ArraySlice, tokenParser: TokenParser, token: Token) throws -> (Expression, Int) { + private static func subExpression(from components: ArraySlice, environment: Environment, token: Token) throws -> (Expression, Int) { var bracketsBalance = 1 let subComponents = components .prefix(while: { @@ -167,7 +167,7 @@ final class IfExpressionParser { throw TemplateSyntaxError("'if' expression error: missing closing bracket") } - let expressionParser = try IfExpressionParser(components: subComponents, tokenParser: tokenParser, token: token) + let expressionParser = try IfExpressionParser(components: subComponents, environment: environment, token: token) let expression = try expressionParser.parse() return (expression, subComponents.count) } @@ -239,7 +239,7 @@ class IfNode : NodeType { var components = token.components() components.removeFirst() - let expression = try parseExpression(components: components, tokenParser: parser, token: token) + let expression = try parser.compileExpression(components: components, token: token) let nodes = try parser.parse(until(["endif", "elif", "else"])) var conditions: [IfCondition] = [ IfCondition(expression: expression, nodes: nodes) @@ -249,7 +249,7 @@ class IfNode : NodeType { while let current = nextToken, current.contents.hasPrefix("elif") { var components = current.components() components.removeFirst() - let expression = try parseExpression(components: components, tokenParser: parser, token: current) + let expression = try parser.compileExpression(components: components, token: current) let nodes = try parser.parse(until(["endif", "elif", "else"])) nextToken = parser.nextToken() @@ -277,7 +277,7 @@ class IfNode : NodeType { var trueNodes = [NodeType]() var falseNodes = [NodeType]() - let expression = try parseExpression(components: components, tokenParser: parser, token: token) + let expression = try parser.compileExpression(components: components, token: token) falseNodes = try parser.parse(until(["endif", "else"])) guard let token = parser.nextToken() else { diff --git a/Sources/Node.swift b/Sources/Node.swift index dc7a72c9..90948209 100644 --- a/Sources/Node.swift +++ b/Sources/Node.swift @@ -74,11 +74,11 @@ public class VariableNode : NodeType { if hasToken("if", at: 1) { let components = components.suffix(from: 2) if let elseIndex = components.index(of: "else") { - condition = try parseExpression(components: Array(components.prefix(upTo: elseIndex)), tokenParser: parser, token: token) + condition = try parser.compileExpression(components: Array(components.prefix(upTo: elseIndex)), token: token) let elseToken = components.suffix(from: elseIndex.advanced(by: 1)).joined(separator: " ") elseExpression = try parser.compileResolvable(elseToken, containedIn: token) } else { - condition = try parseExpression(components: Array(components), tokenParser: parser, token: token) + condition = try parser.compileExpression(components: Array(components), token: token) elseExpression = nil } } else { diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 01335318..6be6632a 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -49,7 +49,7 @@ public class TokenParser { if let tag = token.components().first { do { - let parser = try findTag(name: tag) + let parser = try environment.findTag(name: tag) let node = try parser(self, token) nodes.append(node) } catch { @@ -76,16 +76,16 @@ public class TokenParser { tokens.insert(token, at: 0) } - public func compileFilter(_ token: String) throws -> Resolvable { - return try environment.compileFilter(token) + public func compileFilter(_ filterToken: String, containedIn token: Token) throws -> Resolvable { + return try environment.compileFilter(filterToken, containedIn: token) } - public func compileExpression(components: [String]) throws -> Expression { - return try environment.compileExpression(components: components) + public func compileExpression(components: [String], token: Token) throws -> Expression { + return try environment.compileExpression(components: components, containedIn: token) } - public func compileResolvable(_ token: String) throws -> Resolvable { - return try environment.compileResolvable(token) + public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable { + return try environment.compileResolvable(token, containedIn: containingToken) } } @@ -134,9 +134,13 @@ extension Environment { return filtersWithDistance.filter({ $0.distance == minDistance }).map({ $0.filterName }) } + public func compileFilter(_ token: String) throws -> Resolvable { + return try FilterExpression(token: token, environment: self) + } + public func compileFilter(_ filterToken: String, containedIn containingToken: Token) throws -> Resolvable { do { - return try FilterExpression(token: filterToken, parser: self) + return try FilterExpression(token: filterToken, environment: self) } catch { guard var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil else { throw error @@ -153,26 +157,20 @@ extension Environment { } } - @available(*, deprecated, message: "Use compileFilter(_:containedIn:)") - public func compileFilter(_ token: String) throws -> Resolvable { - return try FilterExpression(token: token, environment: self) - } - - public func compileExpression(components: [String]) throws -> Expression { - return try IfExpressionParser(components: components, environment: self).parse() - } - - @available(*, deprecated, message: "Use compileResolvable(_:containedIn:)") public func compileResolvable(_ token: String) throws -> Resolvable { return try RangeVariable(token, environment: self) ?? compileFilter(token) } public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable { - return try RangeVariable(token, parser: self, containedIn: containingToken) + return try RangeVariable(token, environment: self, containedIn: containingToken) ?? compileFilter(token, containedIn: containingToken) } + public func compileExpression(components: [String], containedIn token: Token) throws -> Expression { + return try IfExpressionParser.parser(components: components, environment: self, token: token).parse() + } + } // https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows diff --git a/Sources/Variable.swift b/Sources/Variable.swift index d6212892..b2531b96 100644 --- a/Sources/Variable.swift +++ b/Sources/Variable.swift @@ -8,8 +8,8 @@ class FilterExpression : Resolvable { let filters: [(FilterType, [Variable])] let variable: Variable - init(token: String, parser: TokenParser) throws { - let bits = token.split(separator: "|").map({ String($0).trim(character: " ") }) + init(token: String, environment: Environment) throws { + let bits = token.smartSplit(separator: "|").map({ String($0).trim(character: " ") }) if bits.isEmpty { throw TemplateSyntaxError("Variable tags must include at least 1 argument") } @@ -144,8 +144,7 @@ public struct RangeVariable: Resolvable { public let from: Resolvable public let to: Resolvable - @available(*, deprecated, message: "Use init?(_:parser:containedIn:)") - public init?(_ token: String, parser: TokenParser) throws { + public init?(_ token: String, environment: Environment) throws { let components = token.components(separatedBy: "...") guard components.count == 2 else { return nil @@ -155,14 +154,14 @@ public struct RangeVariable: Resolvable { self.to = try environment.compileFilter(components[1]) } - public init?(_ token: String, parser: TokenParser, containedIn containingToken: Token) throws { + public init?(_ token: String, environment: Environment, containedIn containingToken: Token) throws { let components = token.components(separatedBy: "...") guard components.count == 2 else { return nil } - self.from = try parser.compileFilter(components[0], containedIn: containingToken) - self.to = try parser.compileFilter(components[1], containedIn: containingToken) + self.from = try environment.compileFilter(components[0], containedIn: containingToken) + self.to = try environment.compileFilter(components[1], containedIn: containingToken) } public func resolve(_ context: Context) throws -> Any? { diff --git a/Tests/StencilTests/ExpressionSpec.swift b/Tests/StencilTests/ExpressionSpec.swift index 0109665a..a115555c 100644 --- a/Tests/StencilTests/ExpressionSpec.swift +++ b/Tests/StencilTests/ExpressionSpec.swift @@ -5,10 +5,9 @@ import Spectre class ExpressionsTests: XCTestCase { func testExpressions() { describe("Expression") { - let parser = TokenParser(tokens: [], environment: Environment()) func parseExpression(components: [String]) throws -> Expression { - let parser = try IfExpressionParser.parser(components: components, tokenParser: parser, token: .text(value: "", at: .unknown)) + let parser = try IfExpressionParser.parser(components: components, environment: Environment(), token: .text(value: "", at: .unknown)) return try parser.parse() } diff --git a/Tests/StencilTests/FilterSpec.swift b/Tests/StencilTests/FilterSpec.swift index 90288298..4a6dedfc 100644 --- a/Tests/StencilTests/FilterSpec.swift +++ b/Tests/StencilTests/FilterSpec.swift @@ -386,28 +386,28 @@ class FilterTests: XCTestCase { """ } } - } - - describe("dynamic filter") { - - $0.it("can apply dynamic filter") { - let template = Template(templateString: "{{ name|filter:somefilter }}") - let result = try template.render(Context(dictionary: ["name": "Jhon", "somefilter": "uppercase"])) - try expect(result) == "JHON" - } - - $0.it("can apply dynamic filter on array") { - let template = Template(templateString: "{{ values|filter:joinfilter }}") - let result = try template.render(Context(dictionary: ["values": [1, 2, 3], "joinfilter": "join:\", \""])) - try expect(result) == "1, 2, 3" - } - $0.it("throws on unknown dynamic filter") { - let template = Template(templateString: "{{ values|filter:unknown }}") - let context = Context(dictionary: ["values": [1, 2, 3], "unknown": "absurd"]) - try expect(try template.render(context)).toThrow() + describe("dynamic filter") { + + $0.it("can apply dynamic filter") { + let template = Template(templateString: "{{ name|filter:somefilter }}") + let result = try template.render(Context(dictionary: ["name": "Jhon", "somefilter": "uppercase"])) + try expect(result) == "JHON" + } + + $0.it("can apply dynamic filter on array") { + let template = Template(templateString: "{{ values|filter:joinfilter }}") + let result = try template.render(Context(dictionary: ["values": [1, 2, 3], "joinfilter": "join:\", \""])) + try expect(result) == "1, 2, 3" + } + + $0.it("throws on unknown dynamic filter") { + let template = Template(templateString: "{{ values|filter:unknown }}") + let context = Context(dictionary: ["values": [1, 2, 3], "unknown": "absurd"]) + try expect(try template.render(context)).toThrow() + } + } } - } diff --git a/Tests/StencilTests/ForNodeSpec.swift b/Tests/StencilTests/ForNodeSpec.swift index 8cd78461..d3bfa8c4 100644 --- a/Tests/StencilTests/ForNodeSpec.swift +++ b/Tests/StencilTests/ForNodeSpec.swift @@ -99,7 +99,8 @@ class ForNodeTests: XCTestCase { $0.it("renders the given nodes while filtering items using where expression") { let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")] - let `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown)) + let parser = TokenParser(tokens: [], environment: Environment()) + let `where` = try parser.compileExpression(components: ["item", ">", "1"], token: .text(value: "", at: .unknown)) let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [], where: `where`) try expect(try node.render(context)) == "2132" } @@ -107,7 +108,8 @@ class ForNodeTests: XCTestCase { $0.it("renders the given empty nodes when all items filtered out with where expression") { let nodes: [NodeType] = [VariableNode(variable: "item")] let emptyNodes: [NodeType] = [TextNode(text: "empty")] - let `where` = try parseExpression(components: ["item", "==", "0"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown)) + let parser = TokenParser(tokens: [], environment: Environment()) + let `where` = try parser.compileExpression(components: ["item", "==", "0"], token: .text(value: "", at: .unknown)) let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes, where: `where`) try expect(try node.render(context)) == "empty" } diff --git a/Tests/StencilTests/VariableSpec.swift b/Tests/StencilTests/VariableSpec.swift index ef2632d2..2f5933dd 100644 --- a/Tests/StencilTests/VariableSpec.swift +++ b/Tests/StencilTests/VariableSpec.swift @@ -351,8 +351,7 @@ class VariableTests: XCTestCase { func makeVariable(_ token: String) throws -> RangeVariable? { let token = Token.variable(value: token, at: .unknown) - let parser = TokenParser(tokens: [token], environment: context.environment) - return try RangeVariable(token.contents, parser: parser, containedIn: token) + return try RangeVariable(token.contents, environment: context.environment, containedIn: token) } $0.it("can resolve closed range as array") { From da6a0ccaca432ad089a6c702137988b3563adfe5 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 1 Oct 2018 22:12:21 +0100 Subject: [PATCH 5/6] added some doc comments --- Sources/Parser.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 6be6632a..c2837749 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -76,14 +76,17 @@ public class TokenParser { tokens.insert(token, at: 0) } + /// Create filter expression from a string contained in provided token public func compileFilter(_ filterToken: String, containedIn token: Token) throws -> Resolvable { return try environment.compileFilter(filterToken, containedIn: token) } + /// Create boolean expression from components contained in provided token public func compileExpression(components: [String], token: Token) throws -> Expression { return try environment.compileExpression(components: components, containedIn: token) } + /// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable { return try environment.compileResolvable(token, containedIn: containingToken) } @@ -134,10 +137,12 @@ extension Environment { return filtersWithDistance.filter({ $0.distance == minDistance }).map({ $0.filterName }) } + /// Create filter expression from a string public func compileFilter(_ token: String) throws -> Resolvable { return try FilterExpression(token: token, environment: self) } + /// Create filter expression from a string contained in provided token public func compileFilter(_ filterToken: String, containedIn containingToken: Token) throws -> Resolvable { do { return try FilterExpression(token: filterToken, environment: self) @@ -157,16 +162,19 @@ extension Environment { } } + /// Create resolvable (i.e. range variable or filter expression) from a string public func compileResolvable(_ token: String) throws -> Resolvable { return try RangeVariable(token, environment: self) ?? compileFilter(token) } + /// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable { return try RangeVariable(token, environment: self, containedIn: containingToken) ?? compileFilter(token, containedIn: containingToken) } + /// Create boolean expression from components contained in provided token public func compileExpression(components: [String], containedIn token: Token) throws -> Expression { return try IfExpressionParser.parser(components: components, environment: self, token: token).parse() } From 019d0cca76dbb727620799f4ebecc6d207bf64f2 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 1 Oct 2018 22:16:43 +0100 Subject: [PATCH 6/6] updated docs --- docs/builtins.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/builtins.rst b/docs/builtins.rst index ad9a8bdd..fe78c6e0 100644 --- a/docs/builtins.rst +++ b/docs/builtins.rst @@ -389,10 +389,10 @@ Filter accepts several arguments: ``filter`` ~~~~~~~~~ -Applies filter by its name, provided as an argument. +Applies the filter with the name provided as an argument to the current expression. .. code-block:: html+django {{ string|filter:myfilter }} -This expression will resolve `myfilter` variable and will apply filter with such name to `string` variable. +This expression will resolve the `myfilter` variable, find a filter named the same as resolved value, and will apply it to the `string` variable. I.e. if `myfilter` variable resolves to string `uppercase` this expression will apply file `uppercase` to `string` variable.