From b4d6a518c7a5c69a2db5327b1561a9a9e0fad6fd Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Wed, 17 Mar 2021 17:02:59 +0000 Subject: [PATCH] Fixed all issues found in spec so far parse has got quite complex might look to simplify --- Sources/HummingbirdMustache/Parser.swift | 13 ++ .../HummingbirdMustache/Template+Parser.swift | 116 ++++++++++++++---- .../MethodTests.swift | 20 +++ .../HummingbirdMustacheTests/SpecTests.swift | 1 + 4 files changed, 129 insertions(+), 21 deletions(-) diff --git a/Sources/HummingbirdMustache/Parser.swift b/Sources/HummingbirdMustache/Parser.swift index 2f7f8c9..87463c0 100644 --- a/Sources/HummingbirdMustache/Parser.swift +++ b/Sources/HummingbirdMustache/Parser.swift @@ -15,6 +15,7 @@ struct HBParser { case unexpected case emptyString case invalidUTF8 + case invalidPosition } /// Create a Parser object @@ -282,6 +283,9 @@ extension HBParser { { unsafeAdvance() } + if startIndex == index { + return subParser(startIndex.. Int { + return index + } + mutating func setPosition(_ index: Int) throws { + guard range.contains(index) else { throw Error.invalidPosition } + guard validateUTF8Character(at: index).0 != nil else { throw Error.invalidPosition } + _setPosition(index) + } } /// extend Parser to conform to Sequence diff --git a/Sources/HummingbirdMustache/Template+Parser.swift b/Sources/HummingbirdMustache/Template+Parser.swift index 7e73237..d15b68d 100644 --- a/Sources/HummingbirdMustache/Template+Parser.swift +++ b/Sources/HummingbirdMustache/Template+Parser.swift @@ -13,73 +13,136 @@ extension HBMustacheTemplate { } /// parse section in mustache text - static func parse(_ parser: inout HBParser, sectionName: String?) throws -> [Token] { + static func parse(_ parser: inout HBParser, sectionName: String?, newLine: Bool = true) throws -> [Token] { var tokens: [Token] = [] + var newLine = newLine + var whiteSpaceBefore: String = "" while !parser.reachedEnd() { - let text = try parser.read(untilString: "{{", throwOnOverflow: false, skipToEnd: true) + // if new line read whitespace + if newLine { + whiteSpaceBefore = parser.read(while: Set(" \t")).string + } + // read until we hit either a newline or "{" + let text = try parser.read(until: Set("{\n"), throwOnOverflow: false) + // if new line append all text read plus newline + if parser.current() == "\n" { + tokens.append(.text(whiteSpaceBefore + text.string + "\n")) + newLine = true + parser.unsafeAdvance() + continue + } else if parser.current() == "{" { + parser.unsafeAdvance() + if parser.current() != "{" { + if text.count > 0 { + tokens.append(.text(whiteSpaceBefore + text.string + "{")) + whiteSpaceBefore = "" + } + continue + } else { + parser.unsafeAdvance() + } + } + if text.count > 0 { - tokens.append(.text(text.string)) + tokens.append(.text(whiteSpaceBefore + text.string)) + whiteSpaceBefore = "" + newLine = false } if parser.reachedEnd() { break } switch parser.current() { case "#": + // section parser.unsafeAdvance() let (name, method) = try parseName(&parser) - if parser.current() == "\n" { - parser.unsafeAdvance() + if newLine && hasLineFinished(&parser) { + newLine = true + if parser.current() == "\n" { + parser.unsafeAdvance() + } + } else if whiteSpaceBefore.count > 0 { + tokens.append(.text(whiteSpaceBefore)) } - let sectionTokens = try parse(&parser, sectionName: name) + let sectionTokens = try parse(&parser, sectionName: name, newLine: newLine) tokens.append(.section(name: name, method: method, template: HBMustacheTemplate(sectionTokens))) case "^": + // inverted section parser.unsafeAdvance() let (name, method) = try parseName(&parser) - if parser.current() == "\n" { - parser.unsafeAdvance() + if newLine && hasLineFinished(&parser) { + newLine = true + if parser.current() == "\n" { + parser.unsafeAdvance() + } + } else if whiteSpaceBefore.count > 0 { + tokens.append(.text(whiteSpaceBefore)) } - let sectionTokens = try parse(&parser, sectionName: name) + let sectionTokens = try parse(&parser, sectionName: name, newLine: newLine) tokens.append(.invertedSection(name: name, method: method, template: HBMustacheTemplate(sectionTokens))) case "/": + // end of section parser.unsafeAdvance() let (name, _) = try parseName(&parser) guard name == sectionName else { throw Error.sectionCloseNameIncorrect } - if parser.current() == "\n" { - parser.unsafeAdvance() + if newLine && hasLineFinished(&parser) { + newLine = true + if parser.current() == "\n" { + parser.unsafeAdvance() + } + } else if whiteSpaceBefore.count > 0 { + tokens.append(.text(whiteSpaceBefore)) } return tokens + case "!": + // comment + parser.unsafeAdvance() + _ = try parseComment(&parser) + if newLine && hasLineFinished(&parser) { + newLine = true + if !parser.reachedEnd() { + parser.unsafeAdvance() + } + } + case "{": + // unescaped variable + if whiteSpaceBefore.count > 0 { + tokens.append(.text(whiteSpaceBefore)) + } parser.unsafeAdvance() let (name, method) = try parseName(&parser) guard try parser.read("}") else { throw Error.unfinishedName } tokens.append(.unescapedVariable(name: name, method: method)) case "&": + // unescaped variable + if whiteSpaceBefore.count > 0 { + tokens.append(.text(whiteSpaceBefore)) + } parser.unsafeAdvance() let (name, method) = try parseName(&parser) tokens.append(.unescapedVariable(name: name, method: method)) - case "!": - parser.unsafeAdvance() - _ = try parseComment(&parser) - if parser.current() == "\n" { - parser.unsafeAdvance() - } - case ">": + // partial + if whiteSpaceBefore.count > 0 { + tokens.append(.text(whiteSpaceBefore)) + } parser.unsafeAdvance() let (name, _) = try parseName(&parser) tokens.append(.partial(name)) - if parser.current() == "\n" { - parser.unsafeAdvance() - } default: + // variable + if whiteSpaceBefore.count > 0 { + tokens.append(.text(whiteSpaceBefore)) + } let (name, method) = try parseName(&parser) tokens.append(.variable(name: name, method: method)) } @@ -118,6 +181,17 @@ extension HBMustacheTemplate { return text.string } + static func hasLineFinished(_ parser: inout HBParser) -> Bool { + var parser2 = parser + if parser.reachedEnd() { return true } + parser2.read(while: Set(" \t\r")) + if parser2.current() == "\n" { + try! parser.setPosition(parser2.getPosition()) + return true + } + return false + } + private static let sectionNameCharsWithoutBrackets = Set("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._?") private static let sectionNameChars = Set("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._?()") } diff --git a/Tests/HummingbirdMustacheTests/MethodTests.swift b/Tests/HummingbirdMustacheTests/MethodTests.swift index a6e9cd0..5fd7fb6 100644 --- a/Tests/HummingbirdMustacheTests/MethodTests.swift +++ b/Tests/HummingbirdMustacheTests/MethodTests.swift @@ -18,11 +18,28 @@ final class MethodTests: XCTestCase { XCTAssertEqual(template.render(object), "TEST") } + func testNewline() 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 + + """) + } + func testFirstLast() throws { let template = try HBMustacheTemplate(string: """ {{#repo}} {{#first()}}first: {{/}}{{#last()}}last: {{/}}{{ name }} {{/repo}} + """) let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]] XCTAssertEqual(template.render(object), """ @@ -38,6 +55,7 @@ final class MethodTests: XCTestCase { {{#repo}} {{#index()}}{{plusone(.)}}{{/}}) {{ name }} {{/repo}} + """) let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]] XCTAssertEqual(template.render(object), """ @@ -53,6 +71,7 @@ final class MethodTests: XCTestCase { {{#repo}} {{index()}}) {{#even()}}even {{/}}{{#odd()}}odd {{/}}{{ name }} {{/repo}} + """) let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]] XCTAssertEqual(template.render(object), """ @@ -68,6 +87,7 @@ final class MethodTests: XCTestCase { {{#reversed(repo)}} {{ name }} {{/repo}} + """) let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]] XCTAssertEqual(template.render(object), """ diff --git a/Tests/HummingbirdMustacheTests/SpecTests.swift b/Tests/HummingbirdMustacheTests/SpecTests.swift index fd090c8..8a2f0b9 100644 --- a/Tests/HummingbirdMustacheTests/SpecTests.swift +++ b/Tests/HummingbirdMustacheTests/SpecTests.swift @@ -390,6 +390,7 @@ final class SpecInvertedTests: XCTestCase { * first * second * third + """ try test(object, template, expected) }