Skip to content

Commit

Permalink
Added map and compactMap filters
Browse files Browse the repository at this point in the history
  • Loading branch information
ilyapuchka committed Mar 22, 2018
1 parent 9184720 commit c789c25
Show file tree
Hide file tree
Showing 11 changed files with 209 additions and 57 deletions.
3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@
- Added support for resolving superclass properties for not-NSObject subclasses
- The `{% for %}` tag can now iterate over tuples, structures and classes via
their stored properties.
- Added `split` filter
- Allow default string filters to be applied to arrays
- Similar filters are suggested when unknown filter is used
- Added `indent` filter
- Allow using new lines inside tags
- Added support for iterating arrays of tuples
- Added `split`, `indent`, `map` and `compact` filters

### Bug Fixes

Expand Down
2 changes: 1 addition & 1 deletion Sources/Expression.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
protocol Expression: CustomStringConvertible {
public protocol Expression: CustomStringConvertible {
func evaluate(context: Context) throws -> Bool
}

Expand Down
16 changes: 11 additions & 5 deletions Sources/Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -59,28 +63,30 @@ class DefaultExtension: Extension {
registerFilter("join", filter: joinFilter)
registerFilter("split", filter: splitFilter)
registerFilter("indent", filter: indentFilter)
registerFilter("map", filter: mapFilter)
registerFilter("compact", filter: compactFilter)
registerFilter("filterEach", filter: filterEachFilter)
}
}


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)
}
}
}
71 changes: 69 additions & 2 deletions Sources/Filters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? "")
Expand All @@ -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 ?? " ")
Expand Down Expand Up @@ -111,3 +111,70 @@ func indent(_ content: String, indentation: String, indentFirst: Bool) -> String
return result.joined(separator: "\n")
}



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.flatMap({ item -> Any? in
if let unwrapped = item, String(describing: unwrapped) != "nil" { return unwrapped }
else { return nil }
})
}

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

let attribute = stringify(arguments[0])
let parser = TokenParser(tokens: [], environment: context.environment)
let expr = try parser.compileExpression(components: Token.block(value: attribute).components())

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

return value
}
2 changes: 1 addition & 1 deletion Sources/ForTag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class ForNode : NodeType {
let filter = try parser.compileFilter(variable)
let `where`: Expression?
if components.count >= 6 {
`where` = try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser)
`where` = try parser.compileExpression(components: Array(components.suffix(from: 5)))
} else {
`where` = nil
}
Expand Down
17 changes: 6 additions & 11 deletions Sources/IfTag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -111,7 +111,8 @@ final class IfExpressionParser {
}
}

return .variable(try tokenParser.compileFilter(component))
let filter = try FilterExpression(token: component, environment: environment)
return .variable(filter)
}
}

Expand Down Expand Up @@ -155,12 +156,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 {
Expand All @@ -187,7 +182,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)
Expand All @@ -197,7 +192,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()
Expand Down Expand Up @@ -236,7 +231,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),
Expand Down
27 changes: 18 additions & 9 deletions Sources/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,16 @@ public class TokenParser {
case .text(let text):
nodes.append(TextNode(text: text))
case .variable:
nodes.append(VariableNode(variable: try compileFilter(token.contents)))
let filter = try FilterExpression(token: token.contents, environment: environment)
nodes.append(VariableNode(variable: filter))
case .block:
if let parse_until = parse_until , parse_until(self, token) {
prependToken(token)
return nodes
}

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:
Expand All @@ -71,8 +72,20 @@ public class TokenParser {
tokens.insert(token, at: 0)
}

public func compileFilter(_ token: String) throws -> Resolvable {
return try FilterExpression(token: token, environment: environment)
}

public func compileExpression(components: [String]) throws -> Expression {
return try IfExpressionParser(components: components, environment: environment).parse()
}

}

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
}
Expand All @@ -82,7 +95,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
}
Expand All @@ -97,7 +110,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)) })
Expand All @@ -110,10 +123,6 @@ public class TokenParser {
return filtersWithDistance.filter({ $0.distance == minDistance }).map({ $0.filterName })
}

public func compileFilter(_ token: String) throws -> Resolvable {
return try FilterExpression(token: token, parser: self)
}

}

// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
Expand Down
8 changes: 4 additions & 4 deletions Sources/Variable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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("")
Expand All @@ -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 {
Expand All @@ -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)
}
}
}
Expand Down
Loading

0 comments on commit c789c25

Please sign in to comment.