diff --git a/Sources/HummingbirdMustache/Template+Parser.swift b/Sources/HummingbirdMustache/Template+Parser.swift index d15b68d..a4f6f73 100644 --- a/Sources/HummingbirdMustache/Template+Parser.swift +++ b/Sources/HummingbirdMustache/Template+Parser.swift @@ -32,10 +32,12 @@ extension HBMustacheTemplate { continue } else if parser.current() == "{" { parser.unsafeAdvance() + // if next character is not "{" then is normal text if parser.current() != "{" { if text.count > 0 { tokens.append(.text(whiteSpaceBefore + text.string + "{")) whiteSpaceBefore = "" + newLine = false } continue } else { @@ -43,26 +45,30 @@ extension HBMustacheTemplate { } } + // whatever text we found before the "{{" should be added if text.count > 0 { tokens.append(.text(whiteSpaceBefore + text.string)) whiteSpaceBefore = "" newLine = false } + // have we reached the end of the text if parser.reachedEnd() { break } + var setNewLine = false switch parser.current() { case "#": // section parser.unsafeAdvance() let (name, method) = try parseName(&parser) if newLine && hasLineFinished(&parser) { - newLine = true + setNewLine = true if parser.current() == "\n" { parser.unsafeAdvance() } } else if whiteSpaceBefore.count > 0 { tokens.append(.text(whiteSpaceBefore)) + whiteSpaceBefore = "" } let sectionTokens = try parse(&parser, sectionName: name, newLine: newLine) tokens.append(.section(name: name, method: method, template: HBMustacheTemplate(sectionTokens))) @@ -72,12 +78,13 @@ extension HBMustacheTemplate { parser.unsafeAdvance() let (name, method) = try parseName(&parser) if newLine && hasLineFinished(&parser) { - newLine = true + setNewLine = true if parser.current() == "\n" { parser.unsafeAdvance() } } else if whiteSpaceBefore.count > 0 { tokens.append(.text(whiteSpaceBefore)) + whiteSpaceBefore = "" } let sectionTokens = try parse(&parser, sectionName: name, newLine: newLine) tokens.append(.invertedSection(name: name, method: method, template: HBMustacheTemplate(sectionTokens))) @@ -90,12 +97,13 @@ extension HBMustacheTemplate { throw Error.sectionCloseNameIncorrect } if newLine && hasLineFinished(&parser) { - newLine = true + setNewLine = true if parser.current() == "\n" { parser.unsafeAdvance() } } else if whiteSpaceBefore.count > 0 { tokens.append(.text(whiteSpaceBefore)) + whiteSpaceBefore = "" } return tokens @@ -104,7 +112,7 @@ extension HBMustacheTemplate { parser.unsafeAdvance() _ = try parseComment(&parser) if newLine && hasLineFinished(&parser) { - newLine = true + setNewLine = true if !parser.reachedEnd() { parser.unsafeAdvance() } @@ -114,6 +122,7 @@ extension HBMustacheTemplate { // unescaped variable if whiteSpaceBefore.count > 0 { tokens.append(.text(whiteSpaceBefore)) + whiteSpaceBefore = "" } parser.unsafeAdvance() let (name, method) = try parseName(&parser) @@ -124,6 +133,7 @@ extension HBMustacheTemplate { // unescaped variable if whiteSpaceBefore.count > 0 { tokens.append(.text(whiteSpaceBefore)) + whiteSpaceBefore = "" } parser.unsafeAdvance() let (name, method) = try parseName(&parser) @@ -131,21 +141,38 @@ extension HBMustacheTemplate { case ">": // partial + parser.unsafeAdvance() + let (name, _) = try parseName(&parser) + /*if newLine && hasLineFinished(&parser) { + setNewLine = true + if parser.current() == "\n" { + parser.unsafeAdvance() + } + }*/ if whiteSpaceBefore.count > 0 { tokens.append(.text(whiteSpaceBefore)) } - parser.unsafeAdvance() - let (name, _) = try parseName(&parser) - tokens.append(.partial(name)) + if newLine && hasLineFinished(&parser) { + setNewLine = true + if parser.current() == "\n" { + parser.unsafeAdvance() + } + tokens.append(.partial(name, indentation: whiteSpaceBefore)) + } else { + tokens.append(.partial(name, indentation: nil)) + } + whiteSpaceBefore = "" default: // variable if whiteSpaceBefore.count > 0 { tokens.append(.text(whiteSpaceBefore)) + whiteSpaceBefore = "" } let (name, method) = try parseName(&parser) tokens.append(.variable(name: name, method: method)) } + newLine = setNewLine } // should never get here if reading section guard sectionName == nil else { diff --git a/Sources/HummingbirdMustache/Template+Render.swift b/Sources/HummingbirdMustache/Template+Render.swift index cb54ce3..870182a 100644 --- a/Sources/HummingbirdMustache/Template+Render.swift +++ b/Sources/HummingbirdMustache/Template+Render.swift @@ -5,9 +5,12 @@ extension HBMustacheTemplate { /// - object: Object /// - context: Context that render is occurring in. Contains information about position in sequence /// - Returns: Rendered text - func render(_ object: Any, context: HBMustacheContext? = nil) -> String { + func render(_ object: Any, context: HBMustacheContext? = nil, indentation: String? = nil) -> String { var string = "" for token in tokens { + if let indentation = indentation, string.last == "\n" { + string += indentation + } switch token { case let .text(text): string += text @@ -31,9 +34,9 @@ extension HBMustacheTemplate { let child = getChild(named: variable, from: object, method: method, context: context) string += renderInvertedSection(child, parent: object, with: template) - case let .partial(name): - if let text = library?.render(object, withTemplate: name) { - string += text + case let .partial(name, indentation): + if let template = library?.getTemplate(named: name) { + string += template.render(object, indentation: indentation) } } } diff --git a/Sources/HummingbirdMustache/Template.swift b/Sources/HummingbirdMustache/Template.swift index d0f23e8..2a63ca0 100644 --- a/Sources/HummingbirdMustache/Template.swift +++ b/Sources/HummingbirdMustache/Template.swift @@ -36,7 +36,7 @@ public final class HBMustacheTemplate { case unescapedVariable(name: String, method: String? = nil) case section(name: String, method: String? = nil, template: HBMustacheTemplate) case invertedSection(name: String, method: String? = nil, template: HBMustacheTemplate) - case partial(String) + case partial(String, indentation: String?) } let tokens: [Token] diff --git a/Tests/HummingbirdMustacheTests/PartialTests.swift b/Tests/HummingbirdMustacheTests/PartialTests.swift index 73fa726..2ec0f90 100644 --- a/Tests/HummingbirdMustacheTests/PartialTests.swift +++ b/Tests/HummingbirdMustacheTests/PartialTests.swift @@ -13,6 +13,7 @@ final class PartialTests: XCTestCase { """) let template2 = try HBMustacheTemplate(string: """ {{.}} + """) library.register(template, named: "base") library.register(template2, named: "user") diff --git a/Tests/HummingbirdMustacheTests/SpecTests.swift b/Tests/HummingbirdMustacheTests/SpecTests.swift index 8a2f0b9..9157b68 100644 --- a/Tests/HummingbirdMustacheTests/SpecTests.swift +++ b/Tests/HummingbirdMustacheTests/SpecTests.swift @@ -525,3 +525,122 @@ final class SpecInvertedTests: XCTestCase { try test(object, template, expected) } } + +// MARK: Partials + +final class SpecPartialsTests: XCTestCase { + func testPartial(_ object: Any, _ template: String, _ partials: [String: String], _ expected: String) throws { + let library = HBMustacheLibrary() + let template = try HBMustacheTemplate(string: template) + library.register(template, named: "template") + for (key, value) in partials { + let template = try HBMustacheTemplate(string: value) + library.register(template, named: key) + } + let result = library.render(object, withTemplate: "template") + XCTAssertEqual(result, expected) + } + + func testBasic() throws { + let object = {} + let template = #""{{>text}}""# + let partial = "from partial" + let expected = #""from partial""# + try testPartial(object, template, ["text": partial], expected) + } + + func testFailedLookup() throws { + let object = {} + let template = #""{{>text}}""# + let expected = "\"\"" + try testPartial(object, template, [:], expected) + } + + func testContext() throws { + let object = ["text": "content"] + let template = #""{{>partial}}""# + let partial = "*{{text}}*" + let expected = #""*content*""# + try testPartial(object, template, ["partial": partial], expected) + } + + func testRecursion() throws { + let object: [String: Any] = ["content": "X", "nodes": [["content": "Y", "nodes": []]]] + let template = #"{{>node}}"# + let partial = "{{content}}<{{#nodes}}{{>node}}{{/nodes}}>" + let expected = #"X>"# + try testPartial(object, template, ["node": partial], expected) + } + + func testSurroundingWhitespace() throws { + let object = {} + let template = "| {{>partial}} |" + let partial = "\t|\t" + let expected = "| \t|\t |" + try testPartial(object, template, ["partial": partial], expected) + } + + func testInlineIdention() throws { + let object = ["data": "|"] + let template = " {{data}} {{> partial}}\n" + let partial = ">\n>" + let expected = " | >\n>\n" + try testPartial(object, template, ["partial": partial], expected) + } + + func testStandaloneLineEndings() throws { + let object = {} + let template = "|\r\n{{>partial}}\r\n|" + let partial = ">" + let expected = "|\r\n>|" + try testPartial(object, template, ["partial": partial], expected) + } + + func testStandaloneWithoutPreviousLine() throws { + let object = {} + let template = " {{>partial}}\n>" + let partial = ">\n>" + let expected = " >\n >>" + try testPartial(object, template, ["partial": partial], expected) + } + + func testStandaloneWithoutNewLine() throws { + let object = {} + let template = ">\n {{>partial}}" + let partial = ">\n>" + let expected = ">\n >\n >" + try testPartial(object, template, ["partial": partial], expected) + } + + func testStandaloneIndentation() throws { + let object = ["content": "<\n->"] + let template = """ + \ + {{>partial}} + / + """ + let partial = """ + | + {{{content}}} + | + + """ + let expected = """ + \ + | + < + -> + | + / + """ + try testPartial(object, template, ["partial": partial], expected) + } + + func testPaddingWhitespace() throws { + let object = ["boolean": true ] + let template = "|{{> partial }}|" + let partial = "[]" + let expected = "|[]|" + try testPartial(object, template, ["partial": partial], expected) + } +} diff --git a/Tests/HummingbirdMustacheTests/TemplateParserTests.swift b/Tests/HummingbirdMustacheTests/TemplateParserTests.swift index 5e84d32..e122399 100644 --- a/Tests/HummingbirdMustacheTests/TemplateParserTests.swift +++ b/Tests/HummingbirdMustacheTests/TemplateParserTests.swift @@ -83,8 +83,8 @@ extension HBMustacheTemplate.Token: Equatable { return lhs1 == rhs1 && lhs2 == rhs2 && lhs3 == rhs3 case let (.invertedSection(lhs1, lhs2, lhs3), .invertedSection(rhs1, rhs2, rhs3)): return lhs1 == rhs1 && lhs2 == rhs2 && lhs3 == rhs3 - case let (.partial(name1), .partial(name2)): - return name1 == name2 + case let (.partial(name1, indent1), .partial(name2, indent2)): + return name1 == name2 && indent1 == indent2 default: return false }