From 3e77076a9ae61d7a3d63ad9cede7086787c15a7f Mon Sep 17 00:00:00 2001 From: Kat Butler Date: Sun, 3 Nov 2019 16:37:28 -0500 Subject: [PATCH 1/3] Support parsing named arguments for filters --- Sources/Liquid/Expression.swift | 9 +++++++++ Sources/Liquid/Parser.swift | 6 ++++-- Sources/Liquid/Value/Value.swift | 22 ++++++++++++++++++++++ Sources/Liquid/Variable.swift | 18 +++++++++++++++++- 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/Sources/Liquid/Expression.swift b/Sources/Liquid/Expression.swift index 3dcdd35..4b75118 100644 --- a/Sources/Liquid/Expression.swift +++ b/Sources/Liquid/Expression.swift @@ -12,6 +12,8 @@ struct Expression: CustomStringConvertible { switch kind { case let .lookup(lookup): return lookup.map { $0.description }.joined(separator: "/") + case .namedParam(let value): + return "\(value.0): \(value.1.description)" case .variable(let key): return key case .filter(let filter): @@ -31,6 +33,7 @@ struct Expression: CustomStringConvertible { indirect enum Kind { case lookup([Expression]) + case namedParam(key: String, expression: Expression) case variable(key: String) case filter(LookupFilter) case value(Value) @@ -63,6 +66,10 @@ struct Expression: CustomStringConvertible { self.kind = .lookup(lookup) } + init(key: String, expression: Expression) { + self.kind = .namedParam(key: key, expression: expression) + } + func evaluate(context: Context) -> Value { return evaluate(context: context, data: nil) } @@ -75,6 +82,8 @@ struct Expression: CustomStringConvertible { for expression in expressions.dropFirst() { result = expression.evaluate(context: context, data: result) } + case let .namedParam(key, expression): + result = Value((key, expression.evaluate(context: context, data: data))) case let .variable(key): if let data = data { result = data.lookup(Value(key), encoder: context.encoder) diff --git a/Sources/Liquid/Parser.swift b/Sources/Liquid/Parser.swift index b92cc03..fd7e9ff 100644 --- a/Sources/Liquid/Parser.swift +++ b/Sources/Liquid/Parser.swift @@ -19,9 +19,11 @@ final class Parser { self.init(tokens: try Lexer.tokenize(string)) } - func look(_ tokenKind: Lexer.Token.Kind) -> Bool { + func look(_ tokenKind: Lexer.Token.Kind, _ skip: Int = 0) -> Bool { guard index != tokens.endIndex else { return false } - return tokens[index].kind == tokenKind + guard (index + skip) < tokens.endIndex else { return false } + + return tokens[index + skip].kind == tokenKind } func consumeId(_ id: String) -> Bool { diff --git a/Sources/Liquid/Value/Value.swift b/Sources/Liquid/Value/Value.swift index f729ba4..f84bf12 100644 --- a/Sources/Liquid/Value/Value.swift +++ b/Sources/Liquid/Value/Value.swift @@ -16,6 +16,7 @@ public final class Value: Equatable, Comparable { case decimal(Decimal) case array([Value]) case dictionary([String: Value]) + case tuple((String, Value)) case drop(Drop) } @@ -57,6 +58,10 @@ public final class Value: Equatable, Comparable { self.init(storage: .dictionary(value)) } + public convenience init(_ value: (String, Value)) { + self.init(storage: .tuple(value)) + } + public convenience init(_ value: Drop) { self.init(storage: .drop(value)) } @@ -144,6 +149,13 @@ public final class Value: Equatable, Comparable { return false } + public var isTuple: Bool { + if case .tuple = storage { + return true + } + return false + } + public var isDrop: Bool { if case .drop = storage { return true @@ -219,6 +231,8 @@ public final class Value: Equatable, Comparable { return "\(value ? "true" : "false")" case .string(let value): return value + case .tuple(let value): + return "\(value.0): \(value.1)" case .int(let value): return "\(value)" case .decimal(let value): @@ -316,6 +330,8 @@ extension Value.Storage: Equatable { return l == r case let (.dictionary(l), .dictionary(r)): return l == r + case let (.tuple(l), .tuple(r)): + return l == r case let (.drop(l), .drop(r)): return l === r default: @@ -328,6 +344,7 @@ extension Value.Storage: Hashable { func hash(into hasher: inout Hasher) { switch self { case .nil: + print("nil here \(#file) \(#line)") break case .bool(let value): hasher.combine(value) @@ -341,6 +358,9 @@ extension Value.Storage: Hashable { hasher.combine(value) case .dictionary(let value): hasher.combine(value) + case .tuple(let value): + hasher.combine(value.0) + hasher.combine(value.1) case .drop(let value): hasher.combine(ObjectIdentifier(value)) } @@ -429,6 +449,8 @@ extension Value.Storage: CustomStringConvertible, CustomDebugStringConvertible { return "array: <\(value)>" case let .dictionary(value): return "dictionary: <\(value)>" + case let .tuple(value): + return "tuple: <\(value)>" case let .drop(value): return "drop: <\(value)>" } diff --git a/Sources/Liquid/Variable.swift b/Sources/Liquid/Variable.swift index 7456199..cb1e1d4 100644 --- a/Sources/Liquid/Variable.swift +++ b/Sources/Liquid/Variable.swift @@ -36,8 +36,24 @@ struct Variable { private func parseFilterArgs(_ parser: Parser) -> [Expression] { var args: [Expression] = [] - + while !parser.look(.endOfString) { + // Assuming all args are named parameters + if parser.look(.id) && parser.look(.colon, 1) { + guard let key = parser.consume(.id) else { fatalError("id should exist") } + + parser.consume(.colon) + let expression = Expression.parse(parser) + args.append(Expression(key: key, expression: expression)) + + if !parser.look(.comma) { + break + } + parser.consume(.comma) + continue + } + + // Assuming all args are ordered parameters args.append(Expression.parse(parser)) if !parser.look(.comma) { break From 700d1439eba2eb2cde0742b67f6b5526f2258755 Mon Sep 17 00:00:00 2001 From: Kat Butler Date: Sun, 3 Nov 2019 17:29:37 -0500 Subject: [PATCH 2/3] Test filter arguments --- Sources/Liquid/Value/Value.swift | 1 - Tests/LiquidTests/FilterTests.swift | 15 +++++++++++++++ Tests/LiquidTests/XCTestCase+Extensions.swift | 4 +++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Sources/Liquid/Value/Value.swift b/Sources/Liquid/Value/Value.swift index f84bf12..c3cf694 100644 --- a/Sources/Liquid/Value/Value.swift +++ b/Sources/Liquid/Value/Value.swift @@ -344,7 +344,6 @@ extension Value.Storage: Hashable { func hash(into hasher: inout Hasher) { switch self { case .nil: - print("nil here \(#file) \(#line)") break case .bool(let value): hasher.combine(value) diff --git a/Tests/LiquidTests/FilterTests.swift b/Tests/LiquidTests/FilterTests.swift index bbef9ff..a0af002 100644 --- a/Tests/LiquidTests/FilterTests.swift +++ b/Tests/LiquidTests/FilterTests.swift @@ -270,4 +270,19 @@ final class FilterTests: XCTestCase { XCTAssertEqual(try Filters.dateFilter(value: Value(1152098955), args: [Value("%m/%d/%Y")], encoder: encoder), Value("07/05/2006")) XCTAssertEqual(try Filters.dateFilter(value: Value("1152098955"), args: [Value("%m/%d/%Y")], encoder: encoder), Value("07/05/2006")) } + + func testFilterArgs() { + let echoFilter: FilterFunc = { (value, args, encoder) -> Value in + let strArgs = args.reduce("", { "\($0), \($1.description)" }) + return Value(value.toString() + " - " + strArgs) + } + + let filters = ["echo": echoFilter] + XCTAssertTemplate("{{ 'testing' | echo: 'Fox Mulder', 1961 }}", "testing - , string: , int: <1961>", filters: filters) + XCTAssertTemplate("{{ 'testing' | echo: name: 'Fox Mulder', yob: 1961 }}", "testing - , tuple: <(\"name\", string: )>, tuple: <(\"yob\", int: <1961>)>", filters: filters) + + let values: [String: Any] = ["name": "Dana Scully", "yob": 1964] + XCTAssertTemplate("{{ 'testing' | echo: name: name, yob: yob }}", "testing - , tuple: <(\"name\", string: )>, tuple: <(\"yob\", int: <1964>)>", values, filters: filters) + XCTAssertTemplate("{{ 'testing' | echo: name, yob }}", "testing - , string: , int: <1964>", values, filters: filters) + } } diff --git a/Tests/LiquidTests/XCTestCase+Extensions.swift b/Tests/LiquidTests/XCTestCase+Extensions.swift index 8ac51a0..d48061e 100644 --- a/Tests/LiquidTests/XCTestCase+Extensions.swift +++ b/Tests/LiquidTests/XCTestCase+Extensions.swift @@ -8,8 +8,10 @@ import XCTest @testable import Liquid -func XCTAssertTemplate(_ templateString: String, _ expression2: String, _ values: [String: Any?] = [:], fileSystem: FileSystem = BlankFileSystem(), _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) { +func XCTAssertTemplate(_ templateString: String, _ expression2: String, _ values: [String: Any?] = [:], fileSystem: FileSystem = BlankFileSystem(), filters: [String: FilterFunc]? = nil, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) { let template = Template(source: templateString, fileSystem: fileSystem) + filters?.forEach({ template.registerFilter(name: $0, filter: $1)}) + XCTAssertNoThrow(try template.parse()) do { let result = try template.render(values: values) From 149795add95d355fce18e1796f7cf2b83b844fa8 Mon Sep 17 00:00:00 2001 From: Kat Butler Date: Wed, 6 Nov 2019 16:40:11 -0500 Subject: [PATCH 3/3] Removed tuple type, introduced kwargs to filter signature --- Sources/Liquid/Expression.swift | 15 +---- Sources/Liquid/Filter+Standard.swift | 90 ++++++++++++++-------------- Sources/Liquid/Filter.swift | 6 +- Sources/Liquid/Value/Value.swift | 21 ------- Sources/Liquid/Variable.swift | 31 ++++------ Tests/LiquidTests/FilterTests.swift | 76 ++++++++++++++--------- 6 files changed, 114 insertions(+), 125 deletions(-) diff --git a/Sources/Liquid/Expression.swift b/Sources/Liquid/Expression.swift index 4b75118..ab8edc1 100644 --- a/Sources/Liquid/Expression.swift +++ b/Sources/Liquid/Expression.swift @@ -12,8 +12,6 @@ struct Expression: CustomStringConvertible { switch kind { case let .lookup(lookup): return lookup.map { $0.description }.joined(separator: "/") - case .namedParam(let value): - return "\(value.0): \(value.1.description)" case .variable(let key): return key case .filter(let filter): @@ -33,7 +31,6 @@ struct Expression: CustomStringConvertible { indirect enum Kind { case lookup([Expression]) - case namedParam(key: String, expression: Expression) case variable(key: String) case filter(LookupFilter) case value(Value) @@ -66,10 +63,6 @@ struct Expression: CustomStringConvertible { self.kind = .lookup(lookup) } - init(key: String, expression: Expression) { - self.kind = .namedParam(key: key, expression: expression) - } - func evaluate(context: Context) -> Value { return evaluate(context: context, data: nil) } @@ -82,8 +75,6 @@ struct Expression: CustomStringConvertible { for expression in expressions.dropFirst() { result = expression.evaluate(context: context, data: result) } - case let .namedParam(key, expression): - result = Value((key, expression.evaluate(context: context, data: data))) case let .variable(key): if let data = data { result = data.lookup(Value(key), encoder: context.encoder) @@ -108,11 +99,11 @@ struct Expression: CustomStringConvertible { } else { switch filter { case .size: - result = try? Filters.sizeFilter(value: data, args: [], encoder: context.encoder) + result = try? Filters.sizeFilter(value: data, args: [], kwargs: [:], encoder: context.encoder) case .first: - result = try? Filters.firstFilter(value: data, args: [], encoder: context.encoder) + result = try? Filters.firstFilter(value: data, args: [], kwargs: [:], encoder: context.encoder) case .last: - result = try? Filters.lastFilter(value: data, args: [], encoder: context.encoder) + result = try? Filters.lastFilter(value: data, args: [], kwargs: [:], encoder: context.encoder) } } } else { diff --git a/Sources/Liquid/Filter+Standard.swift b/Sources/Liquid/Filter+Standard.swift index aaa9777..3de6c87 100644 --- a/Sources/Liquid/Filter+Standard.swift +++ b/Sources/Liquid/Filter+Standard.swift @@ -56,7 +56,7 @@ enum Filters { template.registerFilter(name: "date", filter: dateFilter) } - static func appendFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func appendFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { var result = value.toString() args.forEach { result += $0.toString() @@ -64,28 +64,28 @@ enum Filters { return Value(result) } - static func prependFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func prependFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard let first = args.first, args.count == 1 else { throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName()) } return Value(first.toString() + value.toString()) } - static func downcaseFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func downcaseFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } return Value(value.toString().lowercased()) } - static func upcaseFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func upcaseFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } return Value(value.toString().uppercased()) } - static func capitalizeFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func capitalizeFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } @@ -93,56 +93,56 @@ enum Filters { return Value(string.prefix(1).capitalized + string.dropFirst()) } - static func stripFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func stripFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } return Value(value.toString().strip()) } - static func rstripFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func rstripFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } return Value(value.toString().rstrip()) } - static func lstripFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func lstripFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } return Value(value.toString().lstrip()) } - static func stripNewlinesFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func stripNewlinesFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } return Value(value.toString().replacingOccurrences(of: "\\s", with: "", options: [.regularExpression])) } - static func newlineToBRFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func newlineToBRFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } return Value(value.toString().replacingOccurrences(of: "\n", with: "
\n")) } - static func escapeFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func escapeFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } return Value(value.toString().htmlEscape(decimal: true, useNamedReferences: true)) } - static func escapeOnceFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func escapeOnceFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } return Value(value.toString().htmlUnescape().htmlEscape(decimal: true, useNamedReferences: true)) } - static func urlEncodeFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func urlEncodeFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } @@ -152,7 +152,7 @@ enum Filters { return Value(inputString.addingPercentEncoding(withAllowedCharacters: allowedCharset) ?? inputString) } - static func urlDecodeFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func urlDecodeFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } @@ -160,14 +160,14 @@ enum Filters { return Value(string.removingPercentEncoding ?? string) } - static func stripHTMLFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func stripHTMLFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } return Value(value.toString().replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression)) } - static func truncateFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func truncateFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard !args.isEmpty, let firstArg = args.first else { throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName()) } @@ -179,7 +179,7 @@ enum Filters { return Value(String((string[string.startIndex.. Value { + static func truncateWordsFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard !args.isEmpty, let firstArg = args.first else { throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName()) } @@ -206,7 +206,7 @@ enum Filters { return Value(words.joined(separator: " ").appending(suffix)) } - static func plusFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func plusFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.count == 1, let arg = args.first else { throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName()) } @@ -216,7 +216,7 @@ enum Filters { return Value(value.toDecimal() + arg.toDecimal()) } - static func minusFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func minusFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.count == 1, let arg = args.first else { throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName()) } @@ -226,7 +226,7 @@ enum Filters { return Value(value.toDecimal() - arg.toDecimal()) } - static func multipliedByFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func multipliedByFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.count == 1, let arg = args.first else { throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName()) } @@ -236,7 +236,7 @@ enum Filters { return Value(value.toDecimal() * arg.toDecimal()) } - static func dividedByFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func dividedByFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.count == 1, let arg = args.first else { throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName()) } @@ -246,7 +246,7 @@ enum Filters { return Value(value.toDecimal() / arg.toDecimal()) } - static func absFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func absFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } @@ -256,7 +256,7 @@ enum Filters { return Value(abs(value.toDecimal())) } - static func ceilFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func ceilFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } @@ -266,7 +266,7 @@ enum Filters { return Value(ceil(value.toDecimal().doubleValue)) } - static func floorFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func floorFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } @@ -276,7 +276,7 @@ enum Filters { return Value(floor(value.toDecimal().doubleValue)) } - static func roundFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func roundFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.count <= 1 else { throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName()) } @@ -285,7 +285,7 @@ enum Filters { return Value(result) } - static func moduloFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func moduloFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.count == 1, let arg = args.first else { throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName()) } @@ -296,7 +296,7 @@ enum Filters { return Value(value.toDecimal().doubleValue.truncatingRemainder(dividingBy: arg.toDecimal().doubleValue)) } - static func splitFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func splitFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard let separator = args.first?.toString() else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } @@ -304,14 +304,14 @@ enum Filters { return Value(components.map { Value($0) }) } - static func joinFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func joinFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard let separator = args.first?.toString() else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } return Value(value.toArray().map { $0.toString() }.joined(separator: separator)) } - static func uniqueFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func uniqueFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } @@ -325,26 +325,26 @@ enum Filters { return Value(unique) } - static func sizeFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func sizeFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } return Value(value.size) } - static func firstFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func firstFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } return value.toArray().first ?? Value() } - static func lastFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func lastFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } return value.toArray().last ?? Value() } - static func defaultFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func defaultFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.count == 1, let arg = args.first else { throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName()) } @@ -354,7 +354,7 @@ enum Filters { return value } } - static func replaceFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func replaceFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.count == 2 else { throw RuntimeError.invalidArgCount(expected: 2, received: args.count, tag: tagName()) } @@ -363,7 +363,7 @@ enum Filters { return Value(value.toString().replacingOccurrences(of: target, with: replacement)) } - static func replaceFirstFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func replaceFirstFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.count == 2 else { throw RuntimeError.invalidArgCount(expected: 2, received: args.count, tag: tagName()) } @@ -376,7 +376,7 @@ enum Filters { return Value(string.replacingCharacters(in: range, with: replacement)) } - static func removeFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func removeFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.count == 1 else { throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName()) } @@ -384,7 +384,7 @@ enum Filters { return Value(value.toString().replacingOccurrences(of: target, with: "")) } - static func removeFirstFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func removeFirstFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.count == 1 else { throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName()) } @@ -396,7 +396,7 @@ enum Filters { return Value(string.replacingCharacters(in: range, with: "")) } - static func sliceFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func sliceFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard !args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName()) } @@ -424,21 +424,21 @@ enum Filters { return Value(String(string[sliceStartIndex.. Value { + static func reverseFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } return Value(value.toArray().reversed()) } - static func compactFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func compactFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.isEmpty else { throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName()) } return Value(value.toArray().filter { !$0.isNil }) } - static func mapFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func mapFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.count == 1 else { throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName()) } @@ -448,7 +448,7 @@ enum Filters { return Value(results) } - static func concatFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func concatFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.count == 1 else { throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName()) } @@ -461,7 +461,7 @@ enum Filters { return Value(array) } - static func sortFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func sortFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.count <= 1 else { throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName()) } @@ -475,7 +475,7 @@ enum Filters { } } - static func sortNaturalFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func sortNaturalFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.count <= 1 else { throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName()) } @@ -491,7 +491,7 @@ enum Filters { } } - static func dateFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value { + static func dateFilter(value: Value, args: [Value], kwargs: [String: Value], encoder: Encoder) throws -> Value { guard args.count == 1 else { throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName()) } diff --git a/Sources/Liquid/Filter.swift b/Sources/Liquid/Filter.swift index 22ce0b0..f54153b 100644 --- a/Sources/Liquid/Filter.swift +++ b/Sources/Liquid/Filter.swift @@ -10,11 +10,13 @@ import Foundation struct Filter { let name: String let args: [Expression] + let kwargs: [String: Expression] - init(name: String, args: [Expression]) { + init(name: String, args: [Expression], kwargs: [String: Expression]) { self.name = name self.args = args + self.kwargs = kwargs } } -public typealias FilterFunc = (_ value: Value, _ args: [Value], _ encoder: Encoder) throws -> Value +public typealias FilterFunc = (_ value: Value, _ args: [Value], _ kwargs: [String: Value], _ encoder: Encoder) throws -> Value diff --git a/Sources/Liquid/Value/Value.swift b/Sources/Liquid/Value/Value.swift index c3cf694..f729ba4 100644 --- a/Sources/Liquid/Value/Value.swift +++ b/Sources/Liquid/Value/Value.swift @@ -16,7 +16,6 @@ public final class Value: Equatable, Comparable { case decimal(Decimal) case array([Value]) case dictionary([String: Value]) - case tuple((String, Value)) case drop(Drop) } @@ -58,10 +57,6 @@ public final class Value: Equatable, Comparable { self.init(storage: .dictionary(value)) } - public convenience init(_ value: (String, Value)) { - self.init(storage: .tuple(value)) - } - public convenience init(_ value: Drop) { self.init(storage: .drop(value)) } @@ -149,13 +144,6 @@ public final class Value: Equatable, Comparable { return false } - public var isTuple: Bool { - if case .tuple = storage { - return true - } - return false - } - public var isDrop: Bool { if case .drop = storage { return true @@ -231,8 +219,6 @@ public final class Value: Equatable, Comparable { return "\(value ? "true" : "false")" case .string(let value): return value - case .tuple(let value): - return "\(value.0): \(value.1)" case .int(let value): return "\(value)" case .decimal(let value): @@ -330,8 +316,6 @@ extension Value.Storage: Equatable { return l == r case let (.dictionary(l), .dictionary(r)): return l == r - case let (.tuple(l), .tuple(r)): - return l == r case let (.drop(l), .drop(r)): return l === r default: @@ -357,9 +341,6 @@ extension Value.Storage: Hashable { hasher.combine(value) case .dictionary(let value): hasher.combine(value) - case .tuple(let value): - hasher.combine(value.0) - hasher.combine(value.1) case .drop(let value): hasher.combine(ObjectIdentifier(value)) } @@ -448,8 +429,6 @@ extension Value.Storage: CustomStringConvertible, CustomDebugStringConvertible { return "array: <\(value)>" case let .dictionary(value): return "dictionary: <\(value)>" - case let .tuple(value): - return "tuple: <\(value)>" case let .drop(value): return "drop: <\(value)>" } diff --git a/Sources/Liquid/Variable.swift b/Sources/Liquid/Variable.swift index cb1e1d4..d58fa53 100644 --- a/Sources/Liquid/Variable.swift +++ b/Sources/Liquid/Variable.swift @@ -25,43 +25,38 @@ struct Variable { guard let name = parser.consume(.id) else { break // TODO: throw an error ? } - var args: [Expression]? + + var args: ([Expression], [String: Expression])? + if parser.consume(.colon) != nil { args = parseFilterArgs(parser) } - filters.append(Filter(name: name, args: args ?? [])) + filters.append(Filter(name: name, args: args?.0 ?? [], kwargs: args?.1 ?? [:])) } parser.consume(.endOfString) } - private func parseFilterArgs(_ parser: Parser) -> [Expression] { + private func parseFilterArgs(_ parser: Parser) -> ([Expression], [String:Expression]) { var args: [Expression] = [] + var kwargs: [String: Expression] = [:] while !parser.look(.endOfString) { - // Assuming all args are named parameters if parser.look(.id) && parser.look(.colon, 1) { - guard let key = parser.consume(.id) else { fatalError("id should exist") } + let key = parser.consume(.id)! parser.consume(.colon) - let expression = Expression.parse(parser) - args.append(Expression(key: key, expression: expression)) - - if !parser.look(.comma) { - break - } - parser.consume(.comma) - continue + kwargs[key] = Expression.parse(parser) + } else { + args.append(Expression.parse(parser)) } - - // Assuming all args are ordered parameters - args.append(Expression.parse(parser)) + if !parser.look(.comma) { break } parser.consume(.comma) } - return args + return (args, kwargs) } func evaluate(context: Context) throws -> Value { @@ -70,7 +65,7 @@ struct Variable { guard let filterFunc = context.filter(named: filter.name) else { throw RuntimeError.unknownFilter(filter.name) } - value = try filterFunc(value, filter.args.map { $0.evaluate(context: context) }, context.encoder) + value = try filterFunc(value, filter.args.map { $0.evaluate(context: context) }, filter.kwargs.mapValues { $0.evaluate(context: context) }, context.encoder) } return value } diff --git a/Tests/LiquidTests/FilterTests.swift b/Tests/LiquidTests/FilterTests.swift index a0af002..feb5f2f 100644 --- a/Tests/LiquidTests/FilterTests.swift +++ b/Tests/LiquidTests/FilterTests.swift @@ -54,25 +54,25 @@ final class FilterTests: XCTestCase { } func testEscape() { - XCTAssertEqual(try Filters.escapeFilter(value: Value(""), args: [], encoder: Encoder()).toString(), "<strong>") + XCTAssertEqual(try Filters.escapeFilter(value: Value(""), args: [], kwargs: [:], encoder: Encoder()).toString(), "<strong>") } func testEscapeOnce() throws { - XCTAssertEqual(try Filters.escapeOnceFilter(value: Value("<strong>Hulk"), args: [], encoder: Encoder()).toString(), "<strong>Hulk</strong>") + XCTAssertEqual(try Filters.escapeOnceFilter(value: Value("<strong>Hulk"), args: [], kwargs: [:], encoder: Encoder()).toString(), "<strong>Hulk</strong>") } func testUrlEncode() { - XCTAssertEqual(try Filters.urlEncodeFilter(value: Value("foo+1@example.com"), args: [], encoder: Encoder()).toString(), "foo%2B1%40example.com") + XCTAssertEqual(try Filters.urlEncodeFilter(value: Value("foo+1@example.com"), args: [], kwargs: [:], encoder: Encoder()).toString(), "foo%2B1%40example.com") } func testUrlDecode() { - XCTAssertEqual(try Filters.urlDecodeFilter(value: Value("foo+bar"), args: [], encoder: Encoder()).toString(), "foo bar") - XCTAssertEqual(try Filters.urlDecodeFilter(value: Value("foo%20bar"), args: [], encoder: Encoder()).toString(), "foo bar") - XCTAssertEqual(try Filters.urlDecodeFilter(value: Value("foo%2B1%40example.com"), args: [], encoder: Encoder()).toString(), "foo+1@example.com") + XCTAssertEqual(try Filters.urlDecodeFilter(value: Value("foo+bar"), args: [], kwargs: [:], encoder: Encoder()).toString(), "foo bar") + XCTAssertEqual(try Filters.urlDecodeFilter(value: Value("foo%20bar"), args: [], kwargs: [:], encoder: Encoder()).toString(), "foo bar") + XCTAssertEqual(try Filters.urlDecodeFilter(value: Value("foo%2B1%40example.com"), args: [], kwargs: [:], encoder: Encoder()).toString(), "foo+1@example.com") - XCTAssertEqual(try Filters.urlDecodeFilter(value: Value("%20"), args: [], encoder: Encoder()).toString(), " ") - XCTAssertEqual(try Filters.urlDecodeFilter(value: Value("%2"), args: [], encoder: Encoder()).toString(), "%2") - XCTAssertEqual(try Filters.urlDecodeFilter(value: Value("%"), args: [], encoder: Encoder()).toString(), "%") + XCTAssertEqual(try Filters.urlDecodeFilter(value: Value("%20"), args: [], kwargs: [:], encoder: Encoder()).toString(), " ") + XCTAssertEqual(try Filters.urlDecodeFilter(value: Value("%2"), args: [], kwargs: [:], encoder: Encoder()).toString(), "%2") + XCTAssertEqual(try Filters.urlDecodeFilter(value: Value("%"), args: [], kwargs: [:], encoder: Encoder()).toString(), "%") } func testStripHtml() { @@ -254,35 +254,57 @@ final class FilterTests: XCTestCase { var encoder = Encoder() encoder.locale = Locale(identifier: "en_US") - XCTAssertEqual(try Filters.dateFilter(value: Value("2006-05-05T10:00:00Z"), args: [Value("%B")], encoder: encoder), Value("May")) - XCTAssertEqual(try Filters.dateFilter(value: Value("2006-06-05T10:00:00Z"), args: [Value("%B")], encoder: encoder), Value("June")) - XCTAssertEqual(try Filters.dateFilter(value: Value("2006-07-05T10:00:00Z"), args: [Value("%B")], encoder: encoder), Value("July")) + XCTAssertEqual(try Filters.dateFilter(value: Value("2006-05-05T10:00:00Z"), args: [Value("%B")], kwargs: [:], encoder: encoder), Value("May")) + XCTAssertEqual(try Filters.dateFilter(value: Value("2006-06-05T10:00:00Z"), args: [Value("%B")], kwargs: [:], encoder: encoder), Value("June")) + XCTAssertEqual(try Filters.dateFilter(value: Value("2006-07-05T10:00:00Z"), args: [Value("%B")], kwargs: [:], encoder: encoder), Value("July")) - XCTAssertEqual(try Filters.dateFilter(value: Value("2006-07-05T10:00:00Z"), args: [Value("")], encoder: encoder), Value("7/5/06, 10:00:00 AM")) - XCTAssertEqual(try Filters.dateFilter(value: Value("2006-07-05T10:00:00Z"), args: [Value()], encoder: encoder), Value("7/5/06, 10:00:00 AM")) + XCTAssertEqual(try Filters.dateFilter(value: Value("2006-07-05T10:00:00Z"), args: [Value("")], kwargs: [:], encoder: encoder), Value("7/5/06, 10:00:00 AM")) + XCTAssertEqual(try Filters.dateFilter(value: Value("2006-07-05T10:00:00Z"), args: [Value()], kwargs: [:], encoder: encoder), Value("7/5/06, 10:00:00 AM")) let yearString = "\(Calendar.autoupdatingCurrent.component(.year, from: Date()))" - XCTAssertEqual(try Filters.dateFilter(value: Value("2004-07-16T01:00:00Z"), args: [Value("%m/%d/%Y")], encoder: encoder), Value("07/16/2004")) - XCTAssertEqual(try Filters.dateFilter(value: Value("now"), args: [Value("%Y")], encoder: encoder), Value(yearString)) - XCTAssertEqual(try Filters.dateFilter(value: Value("today"), args: [Value("%Y")], encoder: encoder), Value(yearString)) - XCTAssertEqual(try Filters.dateFilter(value: Value("Today"), args: [Value("%Y")], encoder: encoder), Value(yearString)) + XCTAssertEqual(try Filters.dateFilter(value: Value("2004-07-16T01:00:00Z"), args: [Value("%m/%d/%Y")], kwargs: [:], encoder: encoder), Value("07/16/2004")) + XCTAssertEqual(try Filters.dateFilter(value: Value("now"), args: [Value("%Y")], kwargs: [:], encoder: encoder), Value(yearString)) + XCTAssertEqual(try Filters.dateFilter(value: Value("today"), args: [Value("%Y")], kwargs: [:], encoder: encoder), Value(yearString)) + XCTAssertEqual(try Filters.dateFilter(value: Value("Today"), args: [Value("%Y")], kwargs: [:], encoder: encoder), Value(yearString)) - XCTAssertEqual(try Filters.dateFilter(value: Value(1152098955), args: [Value("%m/%d/%Y")], encoder: encoder), Value("07/05/2006")) - XCTAssertEqual(try Filters.dateFilter(value: Value("1152098955"), args: [Value("%m/%d/%Y")], encoder: encoder), Value("07/05/2006")) + XCTAssertEqual(try Filters.dateFilter(value: Value(1152098955), args: [Value("%m/%d/%Y")], kwargs: [:], encoder: encoder), Value("07/05/2006")) + XCTAssertEqual(try Filters.dateFilter(value: Value("1152098955"), args: [Value("%m/%d/%Y")], kwargs: [:], encoder: encoder), Value("07/05/2006")) } func testFilterArgs() { - let echoFilter: FilterFunc = { (value, args, encoder) -> Value in - let strArgs = args.reduce("", { "\($0), \($1.description)" }) - return Value(value.toString() + " - " + strArgs) + let echoFilter: FilterFunc = { (value, args, kwargs, encoder) -> Value in + let strArgs = args.map { "\($0)" }.sorted() + return Value(value.toString() + " - " + strArgs.description) } let filters = ["echo": echoFilter] - XCTAssertTemplate("{{ 'testing' | echo: 'Fox Mulder', 1961 }}", "testing - , string: , int: <1961>", filters: filters) - XCTAssertTemplate("{{ 'testing' | echo: name: 'Fox Mulder', yob: 1961 }}", "testing - , tuple: <(\"name\", string: )>, tuple: <(\"yob\", int: <1961>)>", filters: filters) + XCTAssertTemplate("{{ 'testing' | echo: 'Fox Mulder', 1961 }}", "testing - [\"int: <1961>\", \"string: \"]", filters: filters) let values: [String: Any] = ["name": "Dana Scully", "yob": 1964] - XCTAssertTemplate("{{ 'testing' | echo: name: name, yob: yob }}", "testing - , tuple: <(\"name\", string: )>, tuple: <(\"yob\", int: <1964>)>", values, filters: filters) - XCTAssertTemplate("{{ 'testing' | echo: name, yob }}", "testing - , string: , int: <1964>", values, filters: filters) + XCTAssertTemplate("{{ 'testing' | echo: name, yob }}", "testing - [\"int: <1964>\", \"string: \"]", values, filters: filters) + } + + func testFilterKWArgs() { + let echoFilter: FilterFunc = { (value, args, kwargs, encoder) -> Value in + let strKWArgs = kwargs.map { "\($0):\($1)" }.sorted() + return Value(value.toString() + " - " + strKWArgs.description) + } + + let filters = ["echo": echoFilter] + XCTAssertTemplate("{{ 'testing' | echo: name: 'Fox Mulder', yob: 1961 }}", "testing - [\"name:string: \", \"yob:int: <1961>\"]", filters: filters) + + let values: [String: Any] = ["name": "Dana Scully", "yob": 1964] + XCTAssertTemplate("{{ 'testing' | echo: name: name, yob: yob }}", "testing - [\"name:string: \", \"yob:int: <1964>\"]", values, filters: filters) + } + + func testFilterOrderedAndNamedArgs() { + let echoFilter: FilterFunc = { (value, args, kwargs, encoder) -> Value in + let strArgs = args.map { "\($0)" }.sorted() + let strKWArgs = kwargs.map { "\($0):\($1)" }.sorted() + return Value(value.toString() + " - " + strArgs.description + strKWArgs.description) + } + + let filters = ["echo": echoFilter] + XCTAssertTemplate("{{ 'testing' | echo: 1, 2, 3, name: 'Fox Mulder', yob: 1961 }}", "testing - [\"int: <1>\", \"int: <2>\", \"int: <3>\"][\"name:string: \", \"yob:int: <1961>\"]", filters: filters) } }