diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e0a2d27..113bc047 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 0.15.2 + +### Enhancements + +- Added support for unique filter using `unique`. This filter allows you to extract unique values from an array + [David Bertet](https://github.com/DavidBertet) + ## 0.15.1 ### Bug Fixes diff --git a/Sources/Stencil/Extension.swift b/Sources/Stencil/Extension.swift index 10586073..7635c9da 100644 --- a/Sources/Stencil/Extension.swift +++ b/Sources/Stencil/Extension.swift @@ -84,6 +84,7 @@ class DefaultExtension: Extension { registerFilter("split", filter: splitFilter) registerFilter("indent", filter: indentFilter) registerFilter("filter", filter: filterFilter) + registerFilter("unique", filter: unique) } } diff --git a/Sources/Stencil/Filters.swift b/Sources/Stencil/Filters.swift index c7a398ea..919e30eb 100644 --- a/Sources/Stencil/Filters.swift +++ b/Sources/Stencil/Filters.swift @@ -137,3 +137,12 @@ func filterFilter(value: Any?, arguments: [Any?], context: Context) throws -> An try expr.resolve(context) } } + +func unique(_ value: Any?) -> Any? { + if let array = value as? [any Hashable] { + var seen: Set = [] + return array.filter { seen.insert(stringify($0)).inserted } + } else { + return value + } +} diff --git a/Tests/StencilTests/EnvironmentBaseAndChildTemplateSpec.swift b/Tests/StencilTests/EnvironmentBaseAndChildTemplateSpec.swift index 036b986a..1b1fdc1e 100644 --- a/Tests/StencilTests/EnvironmentBaseAndChildTemplateSpec.swift +++ b/Tests/StencilTests/EnvironmentBaseAndChildTemplateSpec.swift @@ -33,7 +33,7 @@ final class EnvironmentBaseAndChildTemplateTests: XCTestCase { baseTemplate = try environment.loadTemplate(name: "invalid-base.html") try expectError( - reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + reason: "Unknown filter 'unknown'. Found similar filters: 'unique'.", childToken: "extends \"invalid-base.html\"", baseToken: "target|unknown" ) @@ -67,7 +67,7 @@ final class EnvironmentBaseAndChildTemplateTests: XCTestCase { ) try expectError( - reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + reason: "Unknown filter 'unknown'. Found similar filters: 'unique'.", childToken: "target|unknown", baseToken: nil ) diff --git a/Tests/StencilTests/EnvironmentIncludeTemplateSpec.swift b/Tests/StencilTests/EnvironmentIncludeTemplateSpec.swift index 67ec956e..1c85e9b1 100644 --- a/Tests/StencilTests/EnvironmentIncludeTemplateSpec.swift +++ b/Tests/StencilTests/EnvironmentIncludeTemplateSpec.swift @@ -35,7 +35,7 @@ final class EnvironmentIncludeTemplateTests: XCTestCase { includedTemplate = try environment.loadTemplate(name: "invalid-include.html") try expectError( - reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + reason: "Unknown filter 'unknown'. Found similar filters: 'unique'.", token: #"include "invalid-include.html""#, includedToken: "target|unknown" ) diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 70d7955f..e1d3033d 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -91,7 +91,7 @@ final class EnvironmentTests: XCTestCase { it("reports syntax error in for tag") { self.template = "{% for name in names|unknown %}{{ name }}{% endfor %}" try self.expectError( - reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + reason: "Unknown filter 'unknown'. Found similar filters: 'unique'.", token: "names|unknown" ) } @@ -99,7 +99,7 @@ final class EnvironmentTests: XCTestCase { it("reports syntax error in for-where tag") { self.template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}" try self.expectError( - reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + reason: "Unknown filter 'unknown'. Found similar filters: 'unique'.", token: "name|unknown" ) } @@ -107,7 +107,7 @@ final class EnvironmentTests: XCTestCase { it("reports syntax error in if tag") { self.template = "{% if name|unknown %}{{ name }}{% endif %}" try self.expectError( - reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + reason: "Unknown filter 'unknown'. Found similar filters: 'unique'.", token: "name|unknown" ) } @@ -115,7 +115,7 @@ final class EnvironmentTests: XCTestCase { it("reports syntax error in elif tag") { self.template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}" try self.expectError( - reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + reason: "Unknown filter 'unknown'. Found similar filters: 'unique'.", token: "name|unknown" ) } @@ -123,7 +123,7 @@ final class EnvironmentTests: XCTestCase { it("reports syntax error in ifnot tag") { self.template = "{% ifnot name|unknown %}{{ name }}{% endif %}" try self.expectError( - reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + reason: "Unknown filter 'unknown'. Found similar filters: 'unique'.", token: "name|unknown" ) } @@ -131,7 +131,7 @@ final class EnvironmentTests: XCTestCase { it("reports syntax error in filter tag") { self.template = "{% filter unknown %}Text{% endfilter %}" try self.expectError( - reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + reason: "Unknown filter 'unknown'. Found similar filters: 'unique'.", token: "filter unknown" ) } @@ -139,7 +139,7 @@ final class EnvironmentTests: XCTestCase { it("reports syntax error in variable tag") { self.template = "{{ name|unknown }}" try self.expectError( - reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + reason: "Unknown filter 'unknown'. Found similar filters: 'unique'.", token: "name|unknown" ) } diff --git a/Tests/StencilTests/FilterSpec.swift b/Tests/StencilTests/FilterSpec.swift index 1f88eefc..fc58b7b2 100644 --- a/Tests/StencilTests/FilterSpec.swift +++ b/Tests/StencilTests/FilterSpec.swift @@ -423,6 +423,26 @@ final class FilterTests: XCTestCase { } } + func testUniqueFilter() { + let template = Template(templateString: """ + {{ value|unique }} + """) + + it("collection of strings") { + let result = try template.render(Context(dictionary: ["value": ["One", "Two", "One", "two"]])) + try expect(result) == """ + ["One", "Two", "two"] + """ + } + + it("mixed-type collection") { + let result = try template.render(Context(dictionary: ["value": ["One", 2, true, 2, true, 10.5, "2", "five", "One"]])) + try expect(result) == """ + ["One", 2, true, 10.5, "five"] + """ + } + } + private func expectError( reason: String, token: String, diff --git a/docs/builtins.rst b/docs/builtins.rst index a8a30df0..f984e3ec 100644 --- a/docs/builtins.rst +++ b/docs/builtins.rst @@ -449,3 +449,14 @@ Applies the filter with the name provided as an argument to the current expressi {{ string|filter:myfilter }} 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. + +``unique`` +~~~~~~~~~ + +Returns a list of unique items from the given value. + +.. code-block:: html+django + + {{ value|unique }} + +The unique items are yielded in the same order as their first occurrence in the iterable passed to the filter. \ No newline at end of file