HBTemplate -> HBMustacheTemplate, escape characters

Add tests for mustache examples
This commit is contained in:
Adam Fowler
2021-03-12 07:43:09 +00:00
parent 55245e960f
commit 7f61c8dd72
7 changed files with 143 additions and 95 deletions

View File

@@ -13,12 +13,12 @@ extension Dictionary: HBMustacheParent where Key == String {
} }
protocol HBSequence { protocol HBSequence {
func renderSection(with template: HBTemplate) -> String func renderSection(with template: HBMustacheTemplate) -> String
func renderInvertedSection(with template: HBTemplate) -> String func renderInvertedSection(with template: HBMustacheTemplate) -> String
} }
extension Array: HBSequence { extension Array: HBSequence {
func renderSection(with template: HBTemplate) -> String { func renderSection(with template: HBMustacheTemplate) -> String {
var string = "" var string = ""
for obj in self { for obj in self {
string += template.render(obj) string += template.render(obj)
@@ -26,7 +26,7 @@ extension Array: HBSequence {
return string return string
} }
func renderInvertedSection(with template: HBTemplate) -> String { func renderInvertedSection(with template: HBMustacheTemplate) -> String {
if count == 0 { if count == 0 {
return template.render(self) return template.render(self)
} }
@@ -35,7 +35,7 @@ extension Array: HBSequence {
} }
extension Dictionary: HBSequence { extension Dictionary: HBSequence {
func renderSection(with template: HBTemplate) -> String { func renderSection(with template: HBMustacheTemplate) -> String {
var string = "" var string = ""
for obj in self { for obj in self {
string += template.render(obj) string += template.render(obj)
@@ -43,7 +43,7 @@ extension Dictionary: HBSequence {
return string return string
} }
func renderInvertedSection(with template: HBTemplate) -> String { func renderInvertedSection(with template: HBMustacheTemplate) -> String {
if count == 0 { if count == 0 {
return template.render(self) return template.render(self)
} }

View File

@@ -1,5 +1,5 @@
extension HBTemplate { extension HBMustacheTemplate {
static func parse(_ string: String) throws -> [Token] { static func parse(_ string: String) throws -> [Token] {
var parser = HBParser(string) var parser = HBParser(string)
return try parse(&parser, sectionName: nil) return try parse(&parser, sectionName: nil)
@@ -17,14 +17,20 @@ extension HBTemplate {
case "#": case "#":
parser.unsafeAdvance() parser.unsafeAdvance()
let name = try parseSectionName(&parser) let name = try parseSectionName(&parser)
if parser.current() == "\n" {
parser.unsafeAdvance()
}
let sectionTokens = try parse(&parser, sectionName: name) let sectionTokens = try parse(&parser, sectionName: name)
tokens.append(.section(name, HBTemplate(sectionTokens))) tokens.append(.section(name, HBMustacheTemplate(sectionTokens)))
case "^": case "^":
parser.unsafeAdvance() parser.unsafeAdvance()
let name = try parseSectionName(&parser) let name = try parseSectionName(&parser)
if parser.current() == "\n" {
parser.unsafeAdvance()
}
let sectionTokens = try parse(&parser, sectionName: name) let sectionTokens = try parse(&parser, sectionName: name)
tokens.append(.invertedSection(name, HBTemplate(sectionTokens))) tokens.append(.invertedSection(name, HBMustacheTemplate(sectionTokens)))
case "/": case "/":
parser.unsafeAdvance() parser.unsafeAdvance()
@@ -32,13 +38,16 @@ extension HBTemplate {
guard name == sectionName else { guard name == sectionName else {
throw HBMustacheError.sectionCloseNameIncorrect throw HBMustacheError.sectionCloseNameIncorrect
} }
if parser.current() == "\n" {
parser.unsafeAdvance()
}
return tokens return tokens
case "{": case "{":
parser.unsafeAdvance() parser.unsafeAdvance()
let name = try parseSectionName(&parser) let name = try parseSectionName(&parser)
guard try parser.read("}") else { throw HBMustacheError.unfinishedSectionName } guard try parser.read("}") else { throw HBMustacheError.unfinishedSectionName }
tokens.append(.variable(name)) tokens.append(.unescapedVariable(name))
case "!": case "!":
parser.unsafeAdvance() parser.unsafeAdvance()
@@ -67,5 +76,5 @@ extension HBTemplate {
return text.string return text.string
} }
private static let sectionNameChars = Set<Unicode.Scalar>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.") private static let sectionNameChars = Set<Unicode.Scalar>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._?")
} }

View File

@@ -1,5 +1,5 @@
extension HBTemplate { extension HBMustacheTemplate {
public func render(_ object: Any) -> String { public func render(_ object: Any) -> String {
var string = "" var string = ""
for token in tokens { for token in tokens {
@@ -7,48 +7,32 @@ extension HBTemplate {
case .text(let text): case .text(let text):
string += text string += text
case .variable(let variable): 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) { if let child = getChild(named: variable, from: object) {
string += String(describing: child) string += String(describing: child)
} }
case .section(let variable, let template): case .section(let variable, let template):
let child = getChild(named: variable, from: object) 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): case .invertedSection(let variable, let template):
let child = getChild(named: variable, from: object) let child = getChild(named: variable, from: object)
string += renderInvertedSection(child, with: template) string += renderInvertedSection(child, parent: object, with: template)
} }
} }
return string return string
} }
func renderSection(_ object: Any?, with template: HBTemplate) -> String { func renderSection(_ child: Any?, parent: Any, with template: HBMustacheTemplate) -> String {
switch object { switch child {
case let array as HBSequence: case let array as HBSequence:
return array.renderSection(with: template) return array.renderSection(with: template)
case let bool as Bool: case let bool as Bool:
return bool ? template.render(true) : "" return bool ? template.render(parent) : ""
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) : ""
case .some(let value): case .some(let value):
return template.render(value) return template.render(value)
case .none: case .none:
@@ -56,36 +40,16 @@ extension HBTemplate {
} }
} }
func renderInvertedSection(_ object: Any?, with template: HBTemplate) -> String { func renderInvertedSection(_ child: Any?, parent: Any, with template: HBMustacheTemplate) -> String {
switch object { switch child {
case let array as HBSequence: case let array as HBSequence:
return array.renderInvertedSection(with: template) return array.renderInvertedSection(with: template)
case let bool as Bool: case let bool as Bool:
return bool ? "" : template.render(true) return bool ? "" : template.render(parent)
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) : ""
case .some: case .some:
return "" return ""
case .none: case .none:
return template.render(Void()) return template.render(parent)
} }
} }
@@ -108,6 +72,24 @@ extension HBTemplate {
let nameSplit = name.split(separator: ".").map { String($0) } let nameSplit = name.split(separator: ".").map { String($0) }
return _getChild(named: nameSplit[...], from: object) return _getChild(named: nameSplit[...], from: object)
} }
private static let escapedCharacters: [Character: String] = [
"<": "&lt;",
">": "&gt;",
"&": "&amp;",
]
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? { func unwrap(_ any: Any) -> Any? {

View File

@@ -5,8 +5,8 @@ enum HBMustacheError: Error {
case expectedSectionEnd case expectedSectionEnd
} }
public class HBTemplate { public class HBMustacheTemplate {
public init(_ string: String) throws { public init(string: String) throws {
self.tokens = try Self.parse(string) self.tokens = try Self.parse(string)
} }
@@ -17,8 +17,9 @@ public class HBTemplate {
enum Token { enum Token {
case text(String) case text(String)
case variable(String) case variable(String)
case section(String, HBTemplate) case unescapedVariable(String)
case invertedSection(String, HBTemplate) case section(String, HBMustacheTemplate)
case invertedSection(String, HBMustacheTemplate)
} }
let tokens: [Token] let tokens: [Token]

View File

@@ -3,39 +3,39 @@ import XCTest
final class TemplateParserTests: XCTestCase { final class TemplateParserTests: XCTestCase {
func testText() throws { func testText() throws {
let template = try HBTemplate("test template") let template = try HBMustacheTemplate(string: "test template")
XCTAssertEqual(template.tokens, [.text("test template")]) XCTAssertEqual(template.tokens, [.text("test template")])
} }
func testVariable() throws { func testVariable() throws {
let template = try HBTemplate("test {{variable}}") let template = try HBMustacheTemplate(string: "test {{variable}}")
XCTAssertEqual(template.tokens, [.text("test "), .variable("variable")]) XCTAssertEqual(template.tokens, [.text("test "), .variable("variable")])
} }
func testSection() throws { 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")]))]) XCTAssertEqual(template.tokens, [.text("test "), .section("section", .init([.text("text")]))])
} }
func testInvertedSection() throws { 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")]))]) XCTAssertEqual(template.tokens, [.text("test "), .invertedSection("section", .init([.text("text")]))])
} }
func testComment() throws { func testComment() throws {
let template = try HBTemplate("test {{!section}}") let template = try HBMustacheTemplate(string: "test {{!section}}")
XCTAssertEqual(template.tokens, [.text("test ")]) XCTAssertEqual(template.tokens, [.text("test ")])
} }
} }
extension HBTemplate: Equatable { extension HBMustacheTemplate: Equatable {
public static func == (lhs: HBTemplate, rhs: HBTemplate) -> Bool { public static func == (lhs: HBMustacheTemplate, rhs: HBMustacheTemplate) -> Bool {
lhs.tokens == rhs.tokens lhs.tokens == rhs.tokens
} }
} }
extension HBTemplate.Token: Equatable { extension HBMustacheTemplate.Token: Equatable {
public static func == (lhs: HBTemplate.Token, rhs: HBTemplate.Token) -> Bool { public static func == (lhs: HBMustacheTemplate.Token, rhs: HBMustacheTemplate.Token) -> Bool {
switch (lhs, rhs) { switch (lhs, rhs) {
case (.text(let lhs), .text(let rhs)): case (.text(let lhs), .text(let rhs)):
return lhs == rhs return lhs == rhs

View File

@@ -3,50 +3,50 @@ import XCTest
final class TemplateRendererTests: XCTestCase { final class TemplateRendererTests: XCTestCase {
func testText() throws { func testText() throws {
let template = try HBTemplate("test text") let template = try HBMustacheTemplate(string: "test text")
XCTAssertEqual(template.render("test"), "test text") XCTAssertEqual(template.render("test"), "test text")
} }
func testStringVariable() throws { func testStringVariable() throws {
let template = try HBTemplate("test {{.}}") let template = try HBMustacheTemplate(string: "test {{.}}")
XCTAssertEqual(template.render("text"), "test text") XCTAssertEqual(template.render("text"), "test text")
} }
func testIntegerVariable() throws { func testIntegerVariable() throws {
let template = try HBTemplate("test {{.}}") let template = try HBMustacheTemplate(string: "test {{.}}")
XCTAssertEqual(template.render(101), "test 101") XCTAssertEqual(template.render(101), "test 101")
} }
func testDictionary() throws { 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") XCTAssertEqual(template.render(["value": "test2", "bool": true]), "test test2 true")
} }
func testArraySection() throws { 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": ["test2", "bool"]]), "test *test2*bool")
XCTAssertEqual(template.render(["value": []]), "test ") XCTAssertEqual(template.render(["value": []]), "test ")
} }
func testBooleanSection() throws { 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(true), "test Yep")
XCTAssertEqual(template.render(false), "test ") XCTAssertEqual(template.render(false), "test ")
} }
func testIntegerSection() throws { func testIntegerSection() throws {
let template = try HBTemplate("test {{#.}}{{.}}{{/.}}") let template = try HBMustacheTemplate(string: "test {{#.}}{{.}}{{/.}}")
XCTAssertEqual(template.render(23), "test 23") XCTAssertEqual(template.render(23), "test 23")
XCTAssertEqual(template.render(0), "test ") XCTAssertEqual(template.render(0), "test ")
} }
func testStringSection() throws { func testStringSection() throws {
let template = try HBTemplate("test {{#.}}{{.}}{{/.}}") let template = try HBMustacheTemplate(string: "test {{#.}}{{.}}{{/.}}")
XCTAssertEqual(template.render("Hello"), "test Hello") XCTAssertEqual(template.render("Hello"), "test Hello")
} }
func testInvertedSection() throws { func testInvertedSection() throws {
let template = try HBTemplate("test {{^.}}Inverted{{/.}}") let template = try HBMustacheTemplate(string: "test {{^.}}Inverted{{/.}}")
XCTAssertEqual(template.render(true), "test ") XCTAssertEqual(template.render(true), "test ")
XCTAssertEqual(template.render(false), "test Inverted") XCTAssertEqual(template.render(false), "test Inverted")
} }
@@ -55,7 +55,7 @@ final class TemplateRendererTests: XCTestCase {
struct Test { struct Test {
let string: String 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: "string")), "test string")
} }
@@ -63,7 +63,7 @@ final class TemplateRendererTests: XCTestCase {
struct Test { struct Test {
let string: String? 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: "string")), "test string")
XCTAssertEqual(template.render(Test(string: nil)), "test ") XCTAssertEqual(template.render(Test(string: nil)), "test ")
} }
@@ -72,16 +72,16 @@ final class TemplateRendererTests: XCTestCase {
struct Test { struct Test {
let string: String? 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: "string")), "test *string")
XCTAssertEqual(template.render(Test(string: nil)), "test ") 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: "string")), "test ")
XCTAssertEqual(template2.render(Test(string: nil)), "test *") XCTAssertEqual(template2.render(Test(string: nil)), "test *")
} }
func testDictionarySequence() throws { 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" || XCTAssert(template.render(["one": 1, "two": 2]) == "test 12" ||
template.render(["one": 1, "two": 2]) == "test 21") template.render(["one": 1, "two": 2]) == "test 21")
} }
@@ -94,7 +94,69 @@ final class TemplateRendererTests: XCTestCase {
let test: SubTest 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") 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": "<b>GitHub</b>"]
XCTAssertEqual(template.render(object), """
* Chris
*
* &lt;b&gt;GitHub&lt;/b&gt;
* <b>GitHub</b>
""")
}
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}}
<b>{{name}}</b>
{{/repo}}
""")
let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]]
XCTAssertEqual(template.render(object), """
<b>resque</b>
<b>hub</b>
<b>rip</b>
""")
}
} }

View File

@@ -1,7 +1 @@
import XCTest
import hummingbird_mustacheTests
var tests = [XCTestCaseEntry]()
tests += hummingbird_mustacheTests.allTests()
XCTMain(tests)