diff --git a/Sources/HummingbirdMustache/Method.swift b/Sources/HummingbirdMustache/Method.swift new file mode 100644 index 0000000..29b3f39 --- /dev/null +++ b/Sources/HummingbirdMustache/Method.swift @@ -0,0 +1,29 @@ + +protocol HBMustacheBaseMethods { + func runMethod(_ name: String) -> Any? +} +protocol HBMustacheMethods { + typealias Method = (Self) -> Any + static var methods: [String: Method] { get set } +} + +extension HBMustacheMethods { + static func addMethod(named name: String, method: @escaping Method) { + Self.methods[name] = method + } + func runMethod(_ name: String) -> Any? { + guard let method = Self.methods[name] else { return nil } + return method(self) + } +} + +extension String: HBMustacheBaseMethods { + func runMethod(_ name: String) -> Any? { + switch name { + case "lowercased": + return self.lowercased() + default: + return nil + } + } +} diff --git a/Sources/HummingbirdMustache/Template+Parser.swift b/Sources/HummingbirdMustache/Template+Parser.swift index 8e714b0..ef1ade5 100644 --- a/Sources/HummingbirdMustache/Template+Parser.swift +++ b/Sources/HummingbirdMustache/Template+Parser.swift @@ -24,7 +24,7 @@ extension HBMustacheTemplate { switch parser.current() { case "#": parser.unsafeAdvance() - let name = try parseName(&parser) + let (name, _) = try parseName(&parser) if parser.current() == "\n" { parser.unsafeAdvance() } @@ -33,7 +33,7 @@ extension HBMustacheTemplate { case "^": parser.unsafeAdvance() - let name = try parseName(&parser) + let (name, _) = try parseName(&parser) if parser.current() == "\n" { parser.unsafeAdvance() } @@ -42,7 +42,7 @@ extension HBMustacheTemplate { case "/": parser.unsafeAdvance() - let name = try parseName(&parser) + let (name, _) = try parseName(&parser) guard name == sectionName else { throw Error.sectionCloseNameIncorrect } @@ -53,7 +53,7 @@ extension HBMustacheTemplate { case "{": parser.unsafeAdvance() - let name = try parseName(&parser) + let (name, _) = try parseName(&parser) guard try parser.read("}") else { throw Error.unfinishedName } tokens.append(.unescapedVariable(name)) @@ -63,12 +63,12 @@ extension HBMustacheTemplate { case ">": parser.unsafeAdvance() - let name = try parseName(&parser) + let (name, _) = try parseName(&parser) tokens.append(.partial(name)) default: - let name = try parseName(&parser) - tokens.append(.variable(name)) + let (name, method) = try parseName(&parser) + tokens.append(.variable(name, method)) } } // should never get here if reading section @@ -78,12 +78,24 @@ extension HBMustacheTemplate { return tokens } - static func parseName(_ parser: inout HBParser) throws -> String { + static func parseName(_ parser: inout HBParser) throws -> (String, String?) { parser.read(while: \.isWhitespace) - let text = parser.read(while: sectionNameChars ) + var text = parser.read(while: sectionNameChars ) parser.read(while: \.isWhitespace) guard try parser.read("}"), try parser.read("}") else { throw Error.unfinishedName } - return text.string + // does the name include brackets. If so this is a method call + let string = text.read(while: sectionNameCharsWithoutBrackets) + if text.reachedEnd() { + return (text.string, nil) + } else { + guard text.current() == "(" else { throw Error.unfinishedName } + text.unsafeAdvance() + let string2 = text.read(while: sectionNameCharsWithoutBrackets) + guard text.current() == ")" else { throw Error.unfinishedName } + text.unsafeAdvance() + guard text.reachedEnd() else { throw Error.unfinishedName } + return (string2.string, string.string) + } } static func parseComment(_ parser: inout HBParser) throws -> String { @@ -91,5 +103,6 @@ extension HBMustacheTemplate { return text.string } - private static let sectionNameChars = Set("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._?") + private static let sectionNameCharsWithoutBrackets = Set("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._?") + private static let sectionNameChars = Set("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._?()") } diff --git a/Sources/HummingbirdMustache/Template+Render.swift b/Sources/HummingbirdMustache/Template+Render.swift index f6a3ac2..cf703cf 100644 --- a/Sources/HummingbirdMustache/Template+Render.swift +++ b/Sources/HummingbirdMustache/Template+Render.swift @@ -6,12 +6,18 @@ extension HBMustacheTemplate { switch token { case .text(let text): string += text - case .variable(let variable): - if let child = getChild(named: variable, from: object) { + case .variable(let variable, let method): + if var child = getChild(named: variable, from: object) { + if let method = method, + let runnable = child as? HBMustacheBaseMethods { + if let result = runnable.runMethod(method) { + child = result + } + } if let template = child as? HBMustacheTemplate { string += template.render(object, library: library) } else { - string += encodedEscapedCharacters(String(describing: child)) + string += htmlEscape(String(describing: child)) } } case .unescapedVariable(let variable): @@ -81,16 +87,16 @@ extension HBMustacheTemplate { return _getChild(named: nameSplit[...], from: object) } - private static let escapedCharacters: [Character: String] = [ + private static let htmlEscapedCharacters: [Character: String] = [ "<": "<", ">": ">", "&": "&", ] - func encodedEscapedCharacters(_ string: String) -> String { + func htmlEscape(_ string: String) -> String { var newString = "" newString.reserveCapacity(string.count) for c in string { - if let replacement = Self.escapedCharacters[c] { + if let replacement = Self.htmlEscapedCharacters[c] { newString += replacement } else { newString.append(c) diff --git a/Sources/HummingbirdMustache/Template.swift b/Sources/HummingbirdMustache/Template.swift index 027ce67..cc75280 100644 --- a/Sources/HummingbirdMustache/Template.swift +++ b/Sources/HummingbirdMustache/Template.swift @@ -10,7 +10,7 @@ public class HBMustacheTemplate { enum Token { case text(String) - case variable(String) + case variable(String, String? = nil) case unescapedVariable(String) case section(String, HBMustacheTemplate) case invertedSection(String, HBMustacheTemplate) diff --git a/Tests/HummingbirdMustacheTests/TemplateParserTests.swift b/Tests/HummingbirdMustacheTests/TemplateParserTests.swift index a762d0d..5c48580 100644 --- a/Tests/HummingbirdMustacheTests/TemplateParserTests.swift +++ b/Tests/HummingbirdMustacheTests/TemplateParserTests.swift @@ -77,8 +77,8 @@ extension HBMustacheTemplate.Token: Equatable { switch (lhs, rhs) { case (.text(let lhs), .text(let rhs)): return lhs == rhs - case (.variable(let lhs), .variable(let rhs)): - return lhs == rhs + case (.variable(let lhs, let lhs2), .variable(let rhs, let rhs2)): + return lhs == rhs && lhs2 == rhs2 case (.section(let lhs1, let lhs2), .section(let rhs1, let rhs2)): return lhs1 == rhs1 && lhs2 == rhs2 case (.invertedSection(let lhs1, let lhs2), .invertedSection(let rhs1, let rhs2)): diff --git a/Tests/HummingbirdMustacheTests/TemplateRendererTests.swift b/Tests/HummingbirdMustacheTests/TemplateRendererTests.swift index f3af40a..d985496 100644 --- a/Tests/HummingbirdMustacheTests/TemplateRendererTests.swift +++ b/Tests/HummingbirdMustacheTests/TemplateRendererTests.swift @@ -191,4 +191,32 @@ final class TemplateRendererTests: XCTestCase {

Today.

""") } + + func testLowercased() throws { + let template = try HBMustacheTemplate(string: """ + {{#repo}} + {{ lowercased(name) }} + {{/repo}} + """) + let object: [String: Any] = ["repo": [["name": "Resque"], ["name": "Hub"], ["name": "RIP"]]] + XCTAssertEqual(template.render(object), """ + resque + hub + rip + + """) + } + func testPerformance() throws { + let template = try HBMustacheTemplate(string: """ + {{#repo}} + {{name}} + {{/repo}} + """) + let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]] + let date = Date() + for _ in 1...10000 { + _ = template.render(object) + } + print(-date.timeIntervalSinceNow) + } }