diff --git a/Sources/Liquid/Expression.swift b/Sources/Liquid/Expression.swift index 3dcdd35..ab8edc1 100644 --- a/Sources/Liquid/Expression.swift +++ b/Sources/Liquid/Expression.swift @@ -99,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/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/Variable.swift b/Sources/Liquid/Variable.swift index 7456199..d58fa53 100644 --- a/Sources/Liquid/Variable.swift +++ b/Sources/Liquid/Variable.swift @@ -25,27 +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) { - args.append(Expression.parse(parser)) + if parser.look(.id) && parser.look(.colon, 1) { + let key = parser.consume(.id)! + + parser.consume(.colon) + kwargs[key] = Expression.parse(parser) + } else { + args.append(Expression.parse(parser)) + } + if !parser.look(.comma) { break } parser.consume(.comma) } - return args + return (args, kwargs) } func evaluate(context: Context) throws -> Value { @@ -54,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 bbef9ff..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,20 +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, 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 - [\"int: <1961>\", \"string: \"]", filters: filters) + + let values: [String: Any] = ["name": "Dana Scully", "yob": 1964] + 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) } } 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)