Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ _None_
, i.e. `myfilter = "uppercase"` and then use it to invoke this filter with `{{ string|filter:myfilter }}`.
[Ilya Puchka](https://github.com/ilyapuchka)
[#203](https://github.com/stencilproject/Stencil/pull/203)

- Added `map`, `compact` and `select` filters.
[Ilya Puchka](https://github.com/ilyapuchka)
[#189](https://github.com/stencilproject/Stencil/pull/189)

### Deprecations

Expand Down Expand Up @@ -126,6 +130,7 @@ _None_
[Ilya Puchka](https://github.com/ilyapuchka)
[#167](https://github.com/stencilproject/Stencil/pull/167)


### Bug Fixes

- Fixed using quote as a filter parameter.
Expand Down
3 changes: 3 additions & 0 deletions Sources/Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ class DefaultExtension: Extension {
registerFilter("split", filter: splitFilter)
registerFilter("indent", filter: indentFilter)
registerFilter("filter", filter: filterFilter)
registerFilter("map", filter: mapFilter)
registerFilter("compact", filter: compactFilter)
registerFilter("select", filter: selectFilter)
}
}

Expand Down
68 changes: 68 additions & 0 deletions Sources/Filters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,71 @@ func filterFilter(value: Any?, arguments: [Any?], context: Context) throws -> An
try expr.resolve(context)
}
}

func mapFilter(value: Any?, arguments: [Any?], context: Context) throws -> Any? {
guard arguments.count >= 1 && arguments.count <= 2 else {
throw TemplateSyntaxError("'map' filter takes one or two arguments")
}

let attribute = stringify(arguments[0])
let variable = Variable("map_item.\(attribute)")
let defaultValue = arguments.count == 2 ? arguments[1] : nil

let resolveVariable = { (item: Any) throws -> Any in
let result = try context.push(dictionary: ["map_item": item]) {
try variable.resolve(context)
}
if let result = result { return result }
else if let defaultValue = defaultValue { return defaultValue }
else { return result as Any }
}


if let array = value as? [Any] {
return try array.map(resolveVariable)
} else {
return try resolveVariable(value as Any)
}
}

func compactFilter(value: Any?, arguments: [Any?], context: Context) throws -> Any? {
guard arguments.count <= 1 else {
throw TemplateSyntaxError("'compact' filter takes at most one argument")
}

guard var array = value as? [Any?] else { return value }

if arguments.count == 1 {
guard let mapped = try mapFilter(value: array, arguments: arguments, context: context) as? [Any?] else {
return value
}
array = mapped
}

return array.compactMap { item -> Any? in
guard let unwrapped = item, String(describing: unwrapped) != "nil" else { return nil }
return unwrapped
}
}

func selectFilter(value: Any?, arguments: [Any?], context: Context) throws -> Any? {
guard arguments.count == 1 else {
throw TemplateSyntaxError("'select' filter takes one argument")
}

guard let token = Lexer(templateString: stringify(arguments[0])).tokenize().first else {
throw TemplateSyntaxError("Can't parse filter expression")
}

let expr = try context.environment.compileExpression(components: token.components, containedIn: token)

if let array = value as? [Any] {
return try array.filter {
try context.push(dictionary: ["$0": $0]) {
try expr.evaluate(context: context)
}
}
}

return value
}
6 changes: 3 additions & 3 deletions Sources/Lexer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation

typealias Line = (content: String, number: UInt, range: Range<String.Index>)

struct Lexer {
public struct Lexer {
let templateName: String?
let templateString: String
let lines: [Line]
Expand All @@ -19,7 +19,7 @@ struct Lexer {
"#": "#"
]

init(templateName: String? = nil, templateString: String) {
public init(templateName: String? = nil, templateString: String) {
self.templateName = templateName
self.templateString = templateString

Expand Down Expand Up @@ -74,7 +74,7 @@ struct Lexer {
/// passed on to the parser.
///
/// - Returns: The list of tokens (see `createToken(string: at:)`).
func tokenize() -> [Token] {
public func tokenize() -> [Token] {
var tokens: [Token] = []

let scanner = Scanner(templateString)
Expand Down
1 change: 1 addition & 0 deletions Sources/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ public class TokenParser {
}

extension Environment {

func findTag(name: String) throws -> Extension.TagParser {
for ext in extensions {
if let filter = ext.tags[name] {
Expand Down
11 changes: 11 additions & 0 deletions Tests/StencilTests/EnvironmentSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ final class EnvironmentTests: XCTestCase {
self.template = "{% for name in names %}{{ name }}{% end %}"
try self.expectError(reason: "Unknown template tag 'end'", token: "end")
}

it("reports syntax error on invalid filter expression") {
self.template = "{{ array|select: $0|isPositive }}"
let ext = Extension()
ext.registerFilter("isPositive") { (value) -> Any? in
if let number = toNumber(value: value as Any) { return number > 0 }
else { return nil }
}
self.environment = Environment(extensions: [ext])
try self.expectError(reason: "Can't parse filter expression", token: "array|select: $0|isPositive")
}
}

func testUnknownFilter() {
Expand Down
68 changes: 68 additions & 0 deletions Tests/StencilTests/FilterSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Spectre
import XCTest

final class FilterTests: XCTestCase {

func testRegistration() {
let context: [String: Any] = ["name": "Kyle"]

Expand Down Expand Up @@ -385,6 +386,73 @@ final class FilterTests: XCTestCase {
"""
}

func testMapfilter() throws {
it("can map over attribute") {
let template = Template(templateString: "{{ array|map:\"name\"}}")
let result = try template.render(Context(dictionary: ["array": [["name": "One"], ["name": "Two"], [:]]]))
try expect(result) == "[\"One\", \"Two\", nil]"
}

it("can map over runtime attribute") {
let template = Template(templateString: "{{ array|map:key}}")
let result = try template.render(Context(dictionary: ["key": "name", "array": [["name": "One"], ["name": "Two"]]]))
try expect(result) == "[\"One\", \"Two\"]"
}

it("can use default value") {
let template = Template(templateString: "{{ array|map:\"name\",\"anonymous\"}}")
let result = try template.render(Context(dictionary: ["array": [[:], ["name": "Two"]]]))
try expect(result) == "[\"anonymous\", \"Two\"]"
}

it("can map single value") {
let template = Template(templateString: "{{ value|map:\"user.name\"}}")
let result = try template.render(Context(dictionary: ["value": ["user": ["name": "Two"]]]))
try expect(result) == "Two"
}
}

func testCompactFilter() throws {
it("can filter nil values") {
let template = Template(templateString: "{{ array|compact}}")
let result = try template.render(Context(dictionary: ["array": [nil, "Two"]]))
try expect(result) == "[\"Two\"]"
}

it("can map and filter nil values") {
let template = Template(templateString: "{{ array|compact:\"name\"}}")
let result = try template.render(Context(dictionary: ["array": [["name": "One"], ["name": "Two"], [:]]]))
try expect(result) == "[\"One\", \"Two\"]"
}
}

func testSelectFilter() throws {
it("can filter using filter") {
let ext = Extension()
ext.registerFilter("isPositive") { (value) -> Any? in
if let number = toNumber(value: value as Any) { return number > 0 }
else { return nil }
}
let env = Environment(extensions: [ext])

let template = Template(templateString: "{{ array|select:\"$0|isPositive\" }}")
let result = try template.render(Context(dictionary: ["array": [1, -1, 2, -2, 3, -3]], environment: env))
try expect(result) == "[1, 2, 3]"
}

it("can filter using boolean expression") {
let template = Template(templateString: "{{ array|select:\"$0 > 0\" }}")
let result = try template.render(Context(dictionary: ["array": [1, -1, 2, -2, 3, -3]]))
try expect(result) == "[1, 2, 3]"
}

it("can filter using variable expression") {
let template = Template(templateString: "{{ array|select:\"$0\" }}")
let result = try template.render(Context(dictionary: ["array": [1, -1, nil, 2, -2, 3, -3]]))
try expect(result) == "[1, 2, 3]"
}
}

func testDynamicFilters() throws {
it("can apply dynamic filter") {
let template = Template(templateString: "{{ name|filter:somefilter }}")
Expand Down