From 7f61c8dd72c1a615614f5630acfb47845b5eda1d Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Fri, 12 Mar 2021 07:43:09 +0000 Subject: [PATCH] HBTemplate -> HBMustacheTemplate, escape characters Add tests for mustache examples --- Sources/HummingbirdMustache/Renderable.swift | 12 +-- .../HummingbirdMustache/Template+Parser.swift | 19 +++- .../HummingbirdMustache/Template+Render.swift | 82 +++++++---------- Sources/HummingbirdMustache/Template.swift | 9 +- .../TemplateParserTests.swift | 18 ++-- .../TemplateRendererTests.swift | 92 ++++++++++++++++--- Tests/LinuxMain.swift | 6 -- 7 files changed, 143 insertions(+), 95 deletions(-) diff --git a/Sources/HummingbirdMustache/Renderable.swift b/Sources/HummingbirdMustache/Renderable.swift index caed40d..58581fe 100644 --- a/Sources/HummingbirdMustache/Renderable.swift +++ b/Sources/HummingbirdMustache/Renderable.swift @@ -13,12 +13,12 @@ extension Dictionary: HBMustacheParent where Key == String { } protocol HBSequence { - func renderSection(with template: HBTemplate) -> String - func renderInvertedSection(with template: HBTemplate) -> String + func renderSection(with template: HBMustacheTemplate) -> String + func renderInvertedSection(with template: HBMustacheTemplate) -> String } extension Array: HBSequence { - func renderSection(with template: HBTemplate) -> String { + func renderSection(with template: HBMustacheTemplate) -> String { var string = "" for obj in self { string += template.render(obj) @@ -26,7 +26,7 @@ extension Array: HBSequence { return string } - func renderInvertedSection(with template: HBTemplate) -> String { + func renderInvertedSection(with template: HBMustacheTemplate) -> String { if count == 0 { return template.render(self) } @@ -35,7 +35,7 @@ extension Array: HBSequence { } extension Dictionary: HBSequence { - func renderSection(with template: HBTemplate) -> String { + func renderSection(with template: HBMustacheTemplate) -> String { var string = "" for obj in self { string += template.render(obj) @@ -43,7 +43,7 @@ extension Dictionary: HBSequence { return string } - func renderInvertedSection(with template: HBTemplate) -> String { + func renderInvertedSection(with template: HBMustacheTemplate) -> String { if count == 0 { return template.render(self) } diff --git a/Sources/HummingbirdMustache/Template+Parser.swift b/Sources/HummingbirdMustache/Template+Parser.swift index ce481a3..c469300 100644 --- a/Sources/HummingbirdMustache/Template+Parser.swift +++ b/Sources/HummingbirdMustache/Template+Parser.swift @@ -1,5 +1,5 @@ -extension HBTemplate { +extension HBMustacheTemplate { static func parse(_ string: String) throws -> [Token] { var parser = HBParser(string) return try parse(&parser, sectionName: nil) @@ -17,14 +17,20 @@ extension HBTemplate { case "#": parser.unsafeAdvance() let name = try parseSectionName(&parser) + if parser.current() == "\n" { + parser.unsafeAdvance() + } let sectionTokens = try parse(&parser, sectionName: name) - tokens.append(.section(name, HBTemplate(sectionTokens))) + tokens.append(.section(name, HBMustacheTemplate(sectionTokens))) case "^": parser.unsafeAdvance() let name = try parseSectionName(&parser) + if parser.current() == "\n" { + parser.unsafeAdvance() + } let sectionTokens = try parse(&parser, sectionName: name) - tokens.append(.invertedSection(name, HBTemplate(sectionTokens))) + tokens.append(.invertedSection(name, HBMustacheTemplate(sectionTokens))) case "/": parser.unsafeAdvance() @@ -32,13 +38,16 @@ extension HBTemplate { guard name == sectionName else { throw HBMustacheError.sectionCloseNameIncorrect } + if parser.current() == "\n" { + parser.unsafeAdvance() + } return tokens case "{": parser.unsafeAdvance() let name = try parseSectionName(&parser) guard try parser.read("}") else { throw HBMustacheError.unfinishedSectionName } - tokens.append(.variable(name)) + tokens.append(.unescapedVariable(name)) case "!": parser.unsafeAdvance() @@ -67,5 +76,5 @@ extension HBTemplate { return text.string } - private static let sectionNameChars = Set("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.") + private static let sectionNameChars = Set("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._?") } diff --git a/Sources/HummingbirdMustache/Template+Render.swift b/Sources/HummingbirdMustache/Template+Render.swift index 796f96e..189da34 100644 --- a/Sources/HummingbirdMustache/Template+Render.swift +++ b/Sources/HummingbirdMustache/Template+Render.swift @@ -1,5 +1,5 @@ -extension HBTemplate { +extension HBMustacheTemplate { public func render(_ object: Any) -> String { var string = "" for token in tokens { @@ -7,48 +7,32 @@ extension HBTemplate { case .text(let text): string += text case .variable(let variable): + if let child = getChild(named: variable, from: object) { + string += encodedEscapedCharacters(String(describing: child)) + } + case .unescapedVariable(let variable): if let child = getChild(named: variable, from: object) { string += String(describing: child) } case .section(let variable, let template): let child = getChild(named: variable, from: object) - string += renderSection(child, with: template) + string += renderSection(child, parent: object, with: template) case .invertedSection(let variable, let template): let child = getChild(named: variable, from: object) - string += renderInvertedSection(child, with: template) + string += renderInvertedSection(child, parent: object, with: template) } } return string } - func renderSection(_ object: Any?, with template: HBTemplate) -> String { - switch object { + func renderSection(_ child: Any?, parent: Any, with template: HBMustacheTemplate) -> String { + switch child { case let array as HBSequence: return array.renderSection(with: template) case let bool as Bool: - return bool ? template.render(true) : "" - case let int as Int: - return int != 0 ? template.render(int) : "" - case let int as Int8: - return int != 0 ? template.render(int) : "" - case let int as Int16: - return int != 0 ? template.render(int) : "" - case let int as Int32: - return int != 0 ? template.render(int) : "" - case let int as Int64: - return int != 0 ? template.render(int) : "" - case let int as UInt: - return int != 0 ? template.render(int) : "" - case let int as UInt8: - return int != 0 ? template.render(int) : "" - case let int as UInt16: - return int != 0 ? template.render(int) : "" - case let int as UInt32: - return int != 0 ? template.render(int) : "" - case let int as UInt64: - return int != 0 ? template.render(int) : "" + return bool ? template.render(parent) : "" case .some(let value): return template.render(value) case .none: @@ -56,36 +40,16 @@ extension HBTemplate { } } - func renderInvertedSection(_ object: Any?, with template: HBTemplate) -> String { - switch object { + func renderInvertedSection(_ child: Any?, parent: Any, with template: HBMustacheTemplate) -> String { + switch child { case let array as HBSequence: return array.renderInvertedSection(with: template) case let bool as Bool: - return bool ? "" : template.render(true) - case let int as Int: - return int == 0 ? template.render(int) : "" - case let int as Int8: - return int == 0 ? template.render(int) : "" - case let int as Int16: - return int == 0 ? template.render(int) : "" - case let int as Int32: - return int == 0 ? template.render(int) : "" - case let int as Int64: - return int == 0 ? template.render(int) : "" - case let int as UInt: - return int == 0 ? template.render(int) : "" - case let int as UInt8: - return int == 0 ? template.render(int) : "" - case let int as UInt16: - return int == 0 ? template.render(int) : "" - case let int as UInt32: - return int == 0 ? template.render(int) : "" - case let int as UInt64: - return int == 0 ? template.render(int) : "" + return bool ? "" : template.render(parent) case .some: return "" case .none: - return template.render(Void()) + return template.render(parent) } } @@ -108,6 +72,24 @@ extension HBTemplate { let nameSplit = name.split(separator: ".").map { String($0) } return _getChild(named: nameSplit[...], from: object) } + + private static let escapedCharacters: [Character: String] = [ + "<": "<", + ">": ">", + "&": "&", + ] + func encodedEscapedCharacters(_ string: String) -> String { + var newString = "" + newString.reserveCapacity(string.count) + for c in string { + if let replacement = Self.escapedCharacters[c] { + newString += replacement + } else { + newString.append(c) + } + } + return newString + } } func unwrap(_ any: Any) -> Any? { diff --git a/Sources/HummingbirdMustache/Template.swift b/Sources/HummingbirdMustache/Template.swift index 4adaa39..9559916 100644 --- a/Sources/HummingbirdMustache/Template.swift +++ b/Sources/HummingbirdMustache/Template.swift @@ -5,8 +5,8 @@ enum HBMustacheError: Error { case expectedSectionEnd } -public class HBTemplate { - public init(_ string: String) throws { +public class HBMustacheTemplate { + public init(string: String) throws { self.tokens = try Self.parse(string) } @@ -17,8 +17,9 @@ public class HBTemplate { enum Token { case text(String) case variable(String) - case section(String, HBTemplate) - case invertedSection(String, HBTemplate) + case unescapedVariable(String) + case section(String, HBMustacheTemplate) + case invertedSection(String, HBMustacheTemplate) } let tokens: [Token] diff --git a/Tests/HummingbirdMustacheTests/TemplateParserTests.swift b/Tests/HummingbirdMustacheTests/TemplateParserTests.swift index 6d27fe6..c36daee 100644 --- a/Tests/HummingbirdMustacheTests/TemplateParserTests.swift +++ b/Tests/HummingbirdMustacheTests/TemplateParserTests.swift @@ -3,39 +3,39 @@ import XCTest final class TemplateParserTests: XCTestCase { func testText() throws { - let template = try HBTemplate("test template") + let template = try HBMustacheTemplate(string: "test template") XCTAssertEqual(template.tokens, [.text("test template")]) } func testVariable() throws { - let template = try HBTemplate("test {{variable}}") + let template = try HBMustacheTemplate(string: "test {{variable}}") XCTAssertEqual(template.tokens, [.text("test "), .variable("variable")]) } func testSection() throws { - let template = try HBTemplate("test {{#section}}text{{/section}}") + let template = try HBMustacheTemplate(string: "test {{#section}}text{{/section}}") XCTAssertEqual(template.tokens, [.text("test "), .section("section", .init([.text("text")]))]) } func testInvertedSection() throws { - let template = try HBTemplate("test {{^section}}text{{/section}}") + let template = try HBMustacheTemplate(string: "test {{^section}}text{{/section}}") XCTAssertEqual(template.tokens, [.text("test "), .invertedSection("section", .init([.text("text")]))]) } func testComment() throws { - let template = try HBTemplate("test {{!section}}") + let template = try HBMustacheTemplate(string: "test {{!section}}") XCTAssertEqual(template.tokens, [.text("test ")]) } } -extension HBTemplate: Equatable { - public static func == (lhs: HBTemplate, rhs: HBTemplate) -> Bool { +extension HBMustacheTemplate: Equatable { + public static func == (lhs: HBMustacheTemplate, rhs: HBMustacheTemplate) -> Bool { lhs.tokens == rhs.tokens } } -extension HBTemplate.Token: Equatable { - public static func == (lhs: HBTemplate.Token, rhs: HBTemplate.Token) -> Bool { +extension HBMustacheTemplate.Token: Equatable { + public static func == (lhs: HBMustacheTemplate.Token, rhs: HBMustacheTemplate.Token) -> Bool { switch (lhs, rhs) { case (.text(let lhs), .text(let rhs)): return lhs == rhs diff --git a/Tests/HummingbirdMustacheTests/TemplateRendererTests.swift b/Tests/HummingbirdMustacheTests/TemplateRendererTests.swift index 7ea600e..d0c1543 100644 --- a/Tests/HummingbirdMustacheTests/TemplateRendererTests.swift +++ b/Tests/HummingbirdMustacheTests/TemplateRendererTests.swift @@ -3,50 +3,50 @@ import XCTest final class TemplateRendererTests: XCTestCase { func testText() throws { - let template = try HBTemplate("test text") + let template = try HBMustacheTemplate(string: "test text") XCTAssertEqual(template.render("test"), "test text") } func testStringVariable() throws { - let template = try HBTemplate("test {{.}}") + let template = try HBMustacheTemplate(string: "test {{.}}") XCTAssertEqual(template.render("text"), "test text") } func testIntegerVariable() throws { - let template = try HBTemplate("test {{.}}") + let template = try HBMustacheTemplate(string: "test {{.}}") XCTAssertEqual(template.render(101), "test 101") } func testDictionary() throws { - let template = try HBTemplate("test {{value}} {{bool}}") + let template = try HBMustacheTemplate(string: "test {{value}} {{bool}}") XCTAssertEqual(template.render(["value": "test2", "bool": true]), "test test2 true") } func testArraySection() throws { - let template = try HBTemplate("test {{#value}}*{{.}}{{/value}}") + let template = try HBMustacheTemplate(string: "test {{#value}}*{{.}}{{/value}}") XCTAssertEqual(template.render(["value": ["test2", "bool"]]), "test *test2*bool") XCTAssertEqual(template.render(["value": []]), "test ") } func testBooleanSection() throws { - let template = try HBTemplate("test {{#.}}Yep{{/.}}") + let template = try HBMustacheTemplate(string: "test {{#.}}Yep{{/.}}") XCTAssertEqual(template.render(true), "test Yep") XCTAssertEqual(template.render(false), "test ") } func testIntegerSection() throws { - let template = try HBTemplate("test {{#.}}{{.}}{{/.}}") + let template = try HBMustacheTemplate(string: "test {{#.}}{{.}}{{/.}}") XCTAssertEqual(template.render(23), "test 23") XCTAssertEqual(template.render(0), "test ") } func testStringSection() throws { - let template = try HBTemplate("test {{#.}}{{.}}{{/.}}") + let template = try HBMustacheTemplate(string: "test {{#.}}{{.}}{{/.}}") XCTAssertEqual(template.render("Hello"), "test Hello") } func testInvertedSection() throws { - let template = try HBTemplate("test {{^.}}Inverted{{/.}}") + let template = try HBMustacheTemplate(string: "test {{^.}}Inverted{{/.}}") XCTAssertEqual(template.render(true), "test ") XCTAssertEqual(template.render(false), "test Inverted") } @@ -55,7 +55,7 @@ final class TemplateRendererTests: XCTestCase { struct Test { let string: String } - let template = try HBTemplate("test {{string}}") + let template = try HBMustacheTemplate(string: "test {{string}}") XCTAssertEqual(template.render(Test(string: "string")), "test string") } @@ -63,7 +63,7 @@ final class TemplateRendererTests: XCTestCase { struct Test { let string: String? } - let template = try HBTemplate("test {{string}}") + let template = try HBMustacheTemplate(string: "test {{string}}") XCTAssertEqual(template.render(Test(string: "string")), "test string") XCTAssertEqual(template.render(Test(string: nil)), "test ") } @@ -72,16 +72,16 @@ final class TemplateRendererTests: XCTestCase { struct Test { let string: String? } - let template = try HBTemplate("test {{#string}}*{{.}}{{/string}}") + let template = try HBMustacheTemplate(string: "test {{#string}}*{{.}}{{/string}}") XCTAssertEqual(template.render(Test(string: "string")), "test *string") XCTAssertEqual(template.render(Test(string: nil)), "test ") - let template2 = try HBTemplate("test {{^string}}*{{/string}}") + let template2 = try HBMustacheTemplate(string: "test {{^string}}*{{/string}}") XCTAssertEqual(template2.render(Test(string: "string")), "test ") XCTAssertEqual(template2.render(Test(string: nil)), "test *") } func testDictionarySequence() throws { - let template = try HBTemplate("test {{#.}}{{value}}{{/.}}") + let template = try HBMustacheTemplate(string: "test {{#.}}{{value}}{{/.}}") XCTAssert(template.render(["one": 1, "two": 2]) == "test 12" || template.render(["one": 1, "two": 2]) == "test 21") } @@ -94,7 +94,69 @@ final class TemplateRendererTests: XCTestCase { let test: SubTest } - let template = try HBTemplate("test {{test.string}}") + let template = try HBMustacheTemplate(string: "test {{test.string}}") XCTAssertEqual(template.render(Test(test: .init(string: "sub"))), "test sub") } + + func testMustacheManualExample1() throws { + let template = try HBMustacheTemplate(string: """ + Hello {{name}} + You have just won {{value}} dollars! + {{#in_ca}} + Well, {{taxed_value}} dollars, after taxes. + {{/in_ca}} + """) + let object: [String: Any] = ["name": "Chris", "value": 10000, "taxed_value": 10000 - (10000 * 0.4), "in_ca": true] + XCTAssertEqual(template.render(object), """ + Hello Chris + You have just won 10000 dollars! + Well, 6000.0 dollars, after taxes. + + """) + } + + func testMustacheManualExample2() throws { + let template = try HBMustacheTemplate(string: """ + * {{name}} + * {{age}} + * {{company}} + * {{{company}}} + """) + let object: [String: Any] = ["name": "Chris", "company": "GitHub"] + XCTAssertEqual(template.render(object), """ + * Chris + * + * <b>GitHub</b> + * GitHub + """) + } + + func testMustacheManualExample3() throws { + let template = try HBMustacheTemplate(string: """ + Shown. + {{#person}} + Never shown! + {{/person}} + """) + let object: [String: Any] = ["person": false] + XCTAssertEqual(template.render(object), """ + Shown. + + """) + } + + func testMustacheManualExample4() throws { + let template = try HBMustacheTemplate(string: """ + {{#repo}} + {{name}} + {{/repo}} + """) + let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]] + XCTAssertEqual(template.render(object), """ + resque + hub + rip + + """) + } } diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index d7de6c3..8b13789 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1,7 +1 @@ -import XCTest -import hummingbird_mustacheTests - -var tests = [XCTestCaseEntry]() -tests += hummingbird_mustacheTests.allTests() -XCTMain(tests)