diff --git a/Package.swift b/Package.swift index 837d459..692780d 100644 --- a/Package.swift +++ b/Package.swift @@ -8,13 +8,9 @@ let package = Package( products: [ .library(name: "HummingbirdMustache", targets: ["HummingbirdMustache"]), ], - dependencies: [ - .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "0.6.0"), - ], + dependencies: [], targets: [ - .target(name: "HummingbirdMustache", dependencies: [ - .product(name: "Hummingbird", package: "hummingbird") - ]), + .target(name: "HummingbirdMustache", dependencies: []), .testTarget(name: "HummingbirdMustacheTests", dependencies: ["HummingbirdMustache"]), ] ) diff --git a/Sources/HummingbirdMustache/Parser.swift b/Sources/HummingbirdMustache/Parser.swift new file mode 100644 index 0000000..6deeb3e --- /dev/null +++ b/Sources/HummingbirdMustache/Parser.swift @@ -0,0 +1,651 @@ +// Parser.swift +// +// Half inspired by Reader class from John Sundell's Ink project +// https://github.com/JohnSundell/Ink/blob/master/Sources/Ink/Internal/Reader.swift +// with optimisation working ie removing String and doing my own UTF8 processing inspired by Fabian Fett's work in +// https://github.com/fabianfett/pure-swift-json/blob/master/Sources/PureSwiftJSONParsing/DocumentReader.swift +// +// This is a copy of the parser from Hummingbird. I am not using the version in Hummingbird to avoid the dependency +import Foundation + +/// Reader object for parsing String buffers +struct HBParser { + enum Error: Swift.Error { + case overflow + case unexpected + case emptyString + case invalidUTF8 + } + + /// Create a Parser object + /// - Parameter string: UTF8 data to parse + init?(_ utf8Data: Bytes, validateUTF8: Bool = true) where Bytes.Element == UInt8 { + if let buffer = utf8Data as? [UInt8] { + self.buffer = buffer + } else { + self.buffer = Array(utf8Data) + } + self.index = 0 + self.range = 0.. +} + +// MARK: sub-parsers + +extension HBParser { + /// initialise a parser that parses a section of the buffer attached to another parser + init(_ parser: HBParser, range: Range) { + self.buffer = parser.buffer + self.index = range.startIndex + self.range = range + + precondition(range.startIndex >= 0 && range.endIndex <= self.buffer.endIndex) + precondition(self.buffer[range.startIndex] & 0xC0 != 0x80) // check we arent in the middle of a UTF8 character + } + + /// initialise a parser that parses a section of the buffer attached to this parser + func subParser(_ range: Range) -> HBParser { + return HBParser(self, range: range) + } +} + +extension HBParser { + /// Return current character + /// - Throws: .overflow + /// - Returns: Current character + mutating func character() throws -> Unicode.Scalar { + guard !self.reachedEnd() else { throw Error.overflow } + return unsafeCurrentAndAdvance() + } + + /// Read the current character and return if it is as intended. If character test returns true then move forward 1 + /// - Parameter char: character to compare against + /// - Throws: .overflow + /// - Returns: If current character was the one we expected + mutating func read(_ char: Unicode.Scalar) throws -> Bool { + let initialIndex = self.index + let c = try character() + guard c == char else { self.index = initialIndex; return false } + return true + } + + /// Read the current character and check if it is in a set of characters If character test returns true then move forward 1 + /// - Parameter characterSet: Set of characters to compare against + /// - Throws: .overflow + /// - Returns: If current character is in character set + mutating func read(_ characterSet: Set) throws -> Bool { + let initialIndex = self.index + let c = try character() + guard characterSet.contains(c) else { self.index = initialIndex; return false } + return true + } + + /// Compare characters at current position against provided string. If the characters are the same as string provided advance past string + /// - Parameter string: String to compare against + /// - Throws: .overflow, .emptyString + /// - Returns: If characters at current position equal string + mutating func read(_ string: String) throws -> Bool { + let initialIndex = self.index + guard string.count > 0 else { throw Error.emptyString } + let subString = try read(count: string.count) + guard subString.string == string else { self.index = initialIndex; return false } + return true + } + + /// Read next so many characters from buffer + /// - Parameter count: Number of characters to read + /// - Throws: .overflow + /// - Returns: The string read from the buffer + mutating func read(count: Int) throws -> HBParser { + var count = count + var readEndIndex = self.index + while count > 0 { + guard readEndIndex != self.range.endIndex else { throw Error.overflow } + readEndIndex = skipUTF8Character(at: readEndIndex) + count -= 1 + } + let result = self.subParser(self.index.. HBParser { + let startIndex = self.index + while !self.reachedEnd() { + if unsafeCurrent() == until { + return self.subParser(startIndex.., throwOnOverflow: Bool = true) throws -> HBParser { + let startIndex = self.index + while !self.reachedEnd() { + if characterSet.contains(unsafeCurrent()) { + return self.subParser(startIndex.. Bool, throwOnOverflow: Bool = true) throws -> HBParser { + let startIndex = self.index + while !self.reachedEnd() { + if until(unsafeCurrent()) { + return self.subParser(startIndex.., throwOnOverflow: Bool = true) throws -> HBParser { + let startIndex = self.index + while !self.reachedEnd() { + if unsafeCurrent()[keyPath: keyPath] { + return self.subParser(startIndex.. HBParser { + var untilString = untilString + return try untilString.withUTF8 { utf8 in + guard utf8.count > 0 else { throw Error.emptyString } + let startIndex = index + var foundIndex = index + var untilIndex = 0 + while !reachedEnd() { + if buffer[index] == utf8[untilIndex] { + if untilIndex == 0 { + foundIndex = index + } + untilIndex += 1 + if untilIndex == utf8.endIndex { + unsafeAdvance() + if skipToEnd == false { + index = foundIndex + } + let result = subParser(startIndex.. HBParser { + let startIndex = self.index + self.index = self.range.endIndex + return self.subParser(startIndex.. Int { + var count = 0 + while !self.reachedEnd(), + unsafeCurrent() == `while` + { + unsafeAdvance() + count += 1 + } + return count + } + + /// Read while character at current position is in supplied set + /// - Parameter while: character set to check + /// - Returns: String read from buffer + @discardableResult mutating func read(while characterSet: Set) -> HBParser { + let startIndex = self.index + while !self.reachedEnd(), + characterSet.contains(unsafeCurrent()) + { + unsafeAdvance() + } + return self.subParser(startIndex.. Bool) -> HBParser { + let startIndex = self.index + while !self.reachedEnd(), + `while`(unsafeCurrent()) + { + unsafeAdvance() + } + return self.subParser(startIndex..) -> HBParser { + let startIndex = self.index + while !self.reachedEnd(), + unsafeCurrent()[keyPath: keyPath] + { + unsafeAdvance() + } + return self.subParser(startIndex.. [HBParser] { + var subParsers: [HBParser] = [] + while !self.reachedEnd() { + do { + let section = try read(until: separator) + subParsers.append(section) + unsafeAdvance() + } catch { + if !self.reachedEnd() { + subParsers.append(self.readUntilTheEnd()) + } + } + } + return subParsers + } + + /// Return whether we have reached the end of the buffer + /// - Returns: Have we reached the end + func reachedEnd() -> Bool { + return self.index == self.range.endIndex + } +} + +/// Public versions of internal functions which include tests for overflow +extension HBParser { + /// Return the character at the current position + /// - Throws: .overflow + /// - Returns: Unicode.Scalar + func current() -> Unicode.Scalar { + guard !self.reachedEnd() else { return Unicode.Scalar(0) } + return unsafeCurrent() + } + + /// Move forward one character + /// - Throws: .overflow + mutating func advance() throws { + guard !self.reachedEnd() else { throw Error.overflow } + return self.unsafeAdvance() + } + + /// Move forward so many character + /// - Parameter amount: number of characters to move forward + /// - Throws: .overflow + mutating func advance(by amount: Int) throws { + var amount = amount + while amount > 0 { + guard !self.reachedEnd() else { throw Error.overflow } + self.index = skipUTF8Character(at: self.index) + amount -= 1 + } + } + + /// Move backwards one character + /// - Throws: .overflow + mutating func retreat() throws { + guard self.index > self.range.startIndex else { throw Error.overflow } + self.index = backOneUTF8Character(at: self.index) + } + + /// Move back so many characters + /// - Parameter amount: number of characters to move back + /// - Throws: .overflow + mutating func retreat(by amount: Int) throws { + var amount = amount + while amount > 0 { + guard self.index > self.range.startIndex else { throw Error.overflow } + self.index = backOneUTF8Character(at: self.index) + amount -= 1 + } + } + + mutating func unsafeAdvance() { + self.index = skipUTF8Character(at: self.index) + } + + mutating func unsafeAdvance(by amount: Int) { + var amount = amount + while amount > 0 { + self.index = skipUTF8Character(at: self.index) + amount -= 1 + } + } +} + +/// extend Parser to conform to Sequence +extension HBParser: Sequence { + typealias Element = Unicode.Scalar + + __consuming func makeIterator() -> Iterator { + return Iterator(self) + } + + struct Iterator: IteratorProtocol { + typealias Element = Unicode.Scalar + + var parser: HBParser + + init(_ parser: HBParser) { + self.parser = parser + } + + mutating func next() -> Unicode.Scalar? { + guard !self.parser.reachedEnd() else { return nil } + return self.parser.unsafeCurrentAndAdvance() + } + } +} + +// internal versions without checks +private extension HBParser { + func unsafeCurrent() -> Unicode.Scalar { + return decodeUTF8Character(at: self.index).0 + } + + mutating func unsafeCurrentAndAdvance() -> Unicode.Scalar { + let (unicodeScalar, index) = decodeUTF8Character(at: self.index) + self.index = index + return unicodeScalar + } + + mutating func _setPosition(_ index: Int) { + self.index = index + } + + func makeString(_ bytes: Bytes) -> String where Bytes.Element == UInt8, Bytes.Index == Int { + if let string = bytes.withContiguousStorageIfAvailable({ String(decoding: $0, as: Unicode.UTF8.self) }) { + return string + } else { + return String(decoding: bytes, as: Unicode.UTF8.self) + } + } +} + +// UTF8 parsing +extension HBParser { + func decodeUTF8Character(at index: Int) -> (Unicode.Scalar, Int) { + var index = index + let byte1 = UInt32(buffer[index]) + var value: UInt32 + if byte1 & 0xC0 == 0xC0 { + index += 1 + let byte2 = UInt32(buffer[index] & 0x3F) + if byte1 & 0xE0 == 0xE0 { + index += 1 + let byte3 = UInt32(buffer[index] & 0x3F) + if byte1 & 0xF0 == 0xF0 { + index += 1 + let byte4 = UInt32(buffer[index] & 0x3F) + value = (byte1 & 0x7) << 18 + byte2 << 12 + byte3 << 6 + byte4 + } else { + value = (byte1 & 0xF) << 12 + byte2 << 6 + byte3 + } + } else { + value = (byte1 & 0x1F) << 6 + byte2 + } + } else { + value = byte1 & 0x7F + } + let unicodeScalar = Unicode.Scalar(value)! + return (unicodeScalar, index + 1) + } + + func skipUTF8Character(at index: Int) -> Int { + if self.buffer[index] & 0x80 != 0x80 { return index + 1 } + if self.buffer[index + 1] & 0xC0 == 0x80 { return index + 2 } + if self.buffer[index + 2] & 0xC0 == 0x80 { return index + 3 } + return index + 4 + } + + func backOneUTF8Character(at index: Int) -> Int { + if self.buffer[index - 1] & 0xC0 != 0x80 { return index - 1 } + if self.buffer[index - 2] & 0xC0 != 0x80 { return index - 2 } + if self.buffer[index - 3] & 0xC0 != 0x80 { return index - 3 } + return index - 4 + } + + /// same as `decodeUTF8Character` but adds extra validation, so we can make assumptions later on in decode and skip + func validateUTF8Character(at index: Int) -> (Unicode.Scalar?, Int) { + var index = index + let byte1 = UInt32(buffer[index]) + var value: UInt32 + if byte1 & 0xC0 == 0xC0 { + index += 1 + let byte = UInt32(buffer[index]) + guard byte & 0xC0 == 0x80 else { return (nil, index) } + let byte2 = UInt32(byte & 0x3F) + if byte1 & 0xE0 == 0xE0 { + index += 1 + let byte = UInt32(buffer[index]) + guard byte & 0xC0 == 0x80 else { return (nil, index) } + let byte3 = UInt32(byte & 0x3F) + if byte1 & 0xF0 == 0xF0 { + index += 1 + let byte = UInt32(buffer[index]) + guard byte & 0xC0 == 0x80 else { return (nil, index) } + let byte4 = UInt32(byte & 0x3F) + value = (byte1 & 0x7) << 18 + byte2 << 12 + byte3 << 6 + byte4 + } else { + value = (byte1 & 0xF) << 12 + byte2 << 6 + byte3 + } + } else { + value = (byte1 & 0x1F) << 6 + byte2 + } + } else { + value = byte1 & 0x7F + } + let unicodeScalar = Unicode.Scalar(value) + return (unicodeScalar, index + 1) + } + + /// return if the buffer is valid UTF8 + func validateUTF8() -> Bool { + var index = self.range.startIndex + while index < self.range.endIndex { + let (scalar, newIndex) = self.validateUTF8Character(at: index) + guard scalar != nil else { return false } + index = newIndex + } + return true + } + + private static let asciiHexValues: [UInt8] = [ + /* 00 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* 08 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* 10 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* 18 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* 20 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* 28 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* 30 */ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + /* 38 */ 0x08, 0x09, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* 40 */ 0x80, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x80, + /* 48 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* 50 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* 58 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* 60 */ 0x80, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x80, + /* 68 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* 70 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* 78 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + + /* 80 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* 88 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* 90 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* 98 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* A0 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* A8 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* B0 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* B8 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* C0 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* C8 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* D0 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* D8 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* E0 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* E8 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* F0 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + /* F8 */ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + ] + + /// percent decode UTF8 + func percentDecode() -> String? { + struct DecodeError: Swift.Error {} + func _percentDecode(_ original: ArraySlice, _ bytes: UnsafeMutableBufferPointer) throws -> Int { + var newIndex = 0 + var index = original.startIndex + + while index < original.endIndex { + // if we have found a percent sign + if original[index] == 0x25 { + let high = Self.asciiHexValues[Int(original[index + 1])] + let low = Self.asciiHexValues[Int(original[index + 2])] + index += 3 + if ((high | low) & 0x80) != 0 { + throw DecodeError() + } + bytes[newIndex] = (high << 4) | low + newIndex += 1 + } else { + bytes[newIndex] = original[index] + newIndex += 1 + index += 1 + } + } + return newIndex + } + + guard self.index != self.range.endIndex else { return "" } + do { + if #available(macOS 11, *) { + return try String(unsafeUninitializedCapacity: range.endIndex - index) { bytes -> Int in + return try _percentDecode(self.buffer[self.index.. Any? +} + +extension HBMustacheParent { + // default child to nil + func child(named: String) -> Any? { return nil } +} + +extension Dictionary: HBMustacheParent where Key == String { + func child(named: String) -> Any? { return self[named] } +} + +protocol HBSequence { + func renderSection(with template: HBTemplate) -> String + func renderInvertedSection(with template: HBTemplate) -> String +} + +extension Array: HBSequence { + func renderSection(with template: HBTemplate) -> String { + var string = "" + for obj in self { + string += template.render(obj) + } + return string + } + + func renderInvertedSection(with template: HBTemplate) -> String { + if count == 0 { + return template.render(self) + } + return "" + } +} diff --git a/Sources/HummingbirdMustache/Template+Parser.swift b/Sources/HummingbirdMustache/Template+Parser.swift new file mode 100644 index 0000000..ce481a3 --- /dev/null +++ b/Sources/HummingbirdMustache/Template+Parser.swift @@ -0,0 +1,71 @@ + +extension HBTemplate { + static func parse(_ string: String) throws -> [Token] { + var parser = HBParser(string) + return try parse(&parser, sectionName: nil) + } + + static func parse(_ parser: inout HBParser, sectionName: String?) throws -> [Token] { + var tokens: [Token] = [] + while !parser.reachedEnd() { + let text = try parser.read(untilString: "{{", throwOnOverflow: false, skipToEnd: true) + tokens.append(.text(text.string)) + if parser.reachedEnd() { + break + } + switch parser.current() { + case "#": + parser.unsafeAdvance() + let name = try parseSectionName(&parser) + let sectionTokens = try parse(&parser, sectionName: name) + tokens.append(.section(name, HBTemplate(sectionTokens))) + + case "^": + parser.unsafeAdvance() + let name = try parseSectionName(&parser) + let sectionTokens = try parse(&parser, sectionName: name) + tokens.append(.invertedSection(name, HBTemplate(sectionTokens))) + + case "/": + parser.unsafeAdvance() + let name = try parseSectionName(&parser) + guard name == sectionName else { + throw HBMustacheError.sectionCloseNameIncorrect + } + return tokens + + case "{": + parser.unsafeAdvance() + let name = try parseSectionName(&parser) + guard try parser.read("}") else { throw HBMustacheError.unfinishedSectionName } + tokens.append(.variable(name)) + + case "!": + parser.unsafeAdvance() + _ = try parseSection(&parser) + + default: + let name = try parseSectionName(&parser) + tokens.append(.variable(name)) + } + } + // should never get here if reading section + guard sectionName == nil else { + throw HBMustacheError.expectedSectionEnd + } + return tokens + } + + static func parseSectionName(_ parser: inout HBParser) throws -> String { + let text = parser.read(while: sectionNameChars ) + guard try parser.read("}"), try parser.read("}") else { throw HBMustacheError.unfinishedSectionName } + return text.string + } + + static func parseSection(_ parser: inout HBParser) throws -> String { + let text = try parser.read(untilString: "}}", throwOnOverflow: true, skipToEnd: true) + return text.string + } + + private static let sectionNameChars = Set("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.") +} diff --git a/Sources/HummingbirdMustache/Template+Render.swift b/Sources/HummingbirdMustache/Template+Render.swift new file mode 100644 index 0000000..fa1099f --- /dev/null +++ b/Sources/HummingbirdMustache/Template+Render.swift @@ -0,0 +1,117 @@ + +extension HBTemplate { + public func render(_ object: Any) -> String { + var string = "" + for token in tokens { + switch token { + case .text(let text): + string += text + case .variable(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) + + case .invertedSection(let variable, let template): + let child = getChild(named: variable, from: object) + string += renderInvertedSection(child, with: template) + + } + } + return string + } + + func renderSection(_ object: Any?, with template: HBTemplate) -> String { + switch object { + 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) : "" + case .some(let value): + return template.render(value) + case .none: + return "" + } + } + + func renderInvertedSection(_ object: Any?, with template: HBTemplate) -> String { + switch object { + 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) : "" + case .some: + return "" + case .none: + return template.render(Void()) + } + } + + func getChild(named name: String, from: Any) -> Any? { + if name == "." { return from } + if let customBox = from as? HBMustacheParent { + return customBox.child(named: name) + } + let mirror = Mirror(reflecting: from) + return mirror.getAttribute(forKey: name) + } +} + +func unwrap(_ any: Any) -> Any? { + let mirror = Mirror(reflecting: any) + guard mirror.displayStyle == .optional else { return any } + guard let first = mirror.children.first else { return nil } + return first.value +} + +extension Mirror { + func getAttribute(forKey key: String) -> Any? { + guard let matched = children.filter({ $0.label == key }).first else { + return nil + } + return unwrap(matched.value) + } +} + diff --git a/Sources/HummingbirdMustache/Template.swift b/Sources/HummingbirdMustache/Template.swift new file mode 100644 index 0000000..4adaa39 --- /dev/null +++ b/Sources/HummingbirdMustache/Template.swift @@ -0,0 +1,26 @@ + +enum HBMustacheError: Error { + case sectionCloseNameIncorrect + case unfinishedSectionName + case expectedSectionEnd +} + +public class HBTemplate { + public init(_ string: String) throws { + self.tokens = try Self.parse(string) + } + + internal init(_ tokens: [Token]) { + self.tokens = tokens + } + + enum Token { + case text(String) + case variable(String) + case section(String, HBTemplate) + case invertedSection(String, HBTemplate) + } + + let tokens: [Token] +} + diff --git a/Sources/HummingbirdMustache/template.swift b/Sources/HummingbirdMustache/template.swift deleted file mode 100644 index 5d8b1d3..0000000 --- a/Sources/HummingbirdMustache/template.swift +++ /dev/null @@ -1,77 +0,0 @@ -import Hummingbird - -enum HBMustacheError: Error { - case sectionCloseNameIncorrect - case unfinishedSectionName -} - -class HBTemplate { - init(_ string: String) throws { - self.tokens = try Self.parse(string) - } - - static func parse(_ string: String) throws -> [Token] { - var parser = Parser(string) - return try parse(&parser, sectionName: nil) - } - - static func parse(_ parser: inout Parser, sectionName: String?) throws -> [Token] { - var tokens: [Token] = [] - while !parser.reachedEnd() { - let text = try parser.read(untilString: "{{", throwOnOverflow: false, skipToEnd: true) - tokens.append(.text(text.string)) - switch parser.current() { - case "#": - let name = try readSectionName(&parser) - let sectionTokens = try parse(&parser, sectionName: name) - tokens.append(.section(name, sectionTokens)) - - case "^": - let name = try readSectionName(&parser) - let sectionTokens = try parse(&parser, sectionName: name) - tokens.append(.invertedSection(name, sectionTokens)) - - case "/": - let name = try readSectionName(&parser) - if name != sectionName { - throw HBMustacheError.sectionCloseNameIncorrect - } - return tokens - - case "{": - let name = try readSectionName(&parser) - guard try parser.read("}") else { throw HBMustacheError.unfinishedSectionName } - tokens.append(.variable(name)) - - case "!": - _ = try readSection(&parser) - - default: - let name = try readSectionName(&parser) - tokens.append(.variable(name)) - } - } - return tokens - } - - static func readSectionName(_ parser: inout Parser) throws -> String { - let text = parser.read(while: { $0.isLetter || $0.isNumber } ) - guard try parser.read("}"), try parser.read("}") else { throw HBMustacheError.unfinishedSectionName } - return text.string - } - - static func readSection(_ parser: inout Parser) throws -> String { - let text = try parser.read(untilString: "}}", throwOnOverflow: true, skipToEnd: true) - return text.string - } - - enum Token { - case text(String) - case variable(String) - case section(String, [Token]) - case invertedSection(String, [Token]) - } - - let tokens: [Token] -} - diff --git a/Tests/HummingbirdMustacheTests/TemplateParserTests.swift b/Tests/HummingbirdMustacheTests/TemplateParserTests.swift new file mode 100644 index 0000000..6d27fe6 --- /dev/null +++ b/Tests/HummingbirdMustacheTests/TemplateParserTests.swift @@ -0,0 +1,52 @@ +import XCTest +@testable import HummingbirdMustache + +final class TemplateParserTests: XCTestCase { + func testText() throws { + let template = try HBTemplate("test template") + XCTAssertEqual(template.tokens, [.text("test template")]) + } + + func testVariable() throws { + let template = try HBTemplate("test {{variable}}") + XCTAssertEqual(template.tokens, [.text("test "), .variable("variable")]) + } + + func testSection() throws { + let template = try HBTemplate("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}}") + XCTAssertEqual(template.tokens, [.text("test "), .invertedSection("section", .init([.text("text")]))]) + } + + func testComment() throws { + let template = try HBTemplate("test {{!section}}") + XCTAssertEqual(template.tokens, [.text("test ")]) + } +} + +extension HBTemplate: Equatable { + public static func == (lhs: HBTemplate, rhs: HBTemplate) -> Bool { + lhs.tokens == rhs.tokens + } +} + +extension HBTemplate.Token: Equatable { + public static func == (lhs: HBTemplate.Token, rhs: HBTemplate.Token) -> Bool { + switch (lhs, rhs) { + case (.text(let lhs), .text(let rhs)): + return lhs == rhs + case (.variable(let lhs), .variable(let rhs)): + return lhs == rhs + 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)): + return lhs1 == rhs1 && lhs2 == rhs2 + default: + return false + } + } +} diff --git a/Tests/HummingbirdMustacheTests/TemplateRendererTests.swift b/Tests/HummingbirdMustacheTests/TemplateRendererTests.swift new file mode 100644 index 0000000..f8d8aab --- /dev/null +++ b/Tests/HummingbirdMustacheTests/TemplateRendererTests.swift @@ -0,0 +1,82 @@ +import XCTest +@testable import HummingbirdMustache + +final class TemplateRendererTests: XCTestCase { + func testText() throws { + let template = try HBTemplate("test text") + XCTAssertEqual(template.render("test"), "test text") + } + + func testStringVariable() throws { + let template = try HBTemplate("test {{.}}") + XCTAssertEqual(template.render("text"), "test text") + } + + func testIntegerVariable() throws { + let template = try HBTemplate("test {{.}}") + XCTAssertEqual(template.render(101), "test 101") + } + + func testDictionary() throws { + let template = try HBTemplate("test {{value}} {{bool}}") + XCTAssertEqual(template.render(["value": "test2", "bool": true]), "test test2 true") + } + + func testArraySection() throws { + let template = try HBTemplate("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{{/.}}") + XCTAssertEqual(template.render(true), "test Yep") + XCTAssertEqual(template.render(false), "test ") + } + + func testIntegerSection() throws { + let template = try HBTemplate("test {{#.}}{{.}}{{/.}}") + XCTAssertEqual(template.render(23), "test 23") + XCTAssertEqual(template.render(0), "test ") + } + + func testStringSection() throws { + let template = try HBTemplate("test {{#.}}{{.}}{{/.}}") + XCTAssertEqual(template.render("Hello"), "test Hello") + } + + func testInvertedSection() throws { + let template = try HBTemplate("test {{^.}}Inverted{{/.}}") + XCTAssertEqual(template.render(true), "test ") + XCTAssertEqual(template.render(false), "test Inverted") + } + + func testMirror() throws { + struct Test { + let string: String + } + let template = try HBTemplate("test {{string}}") + XCTAssertEqual(template.render(Test(string: "string")), "test string") + } + + func testOptionalMirror() throws { + struct Test { + let string: String? + } + let template = try HBTemplate("test {{string}}") + XCTAssertEqual(template.render(Test(string: "string")), "test string") + XCTAssertEqual(template.render(Test(string: nil)), "test ") + } + + func testOptionalSequence() throws { + struct Test { + let string: String? + } + let template = try HBTemplate("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}}") + XCTAssertEqual(template2.render(Test(string: "string")), "test ") + XCTAssertEqual(template2.render(Test(string: nil)), "test *") + } +} diff --git a/Tests/HummingbirdMustacheTests/hummingbird_mustacheTests.swift b/Tests/HummingbirdMustacheTests/hummingbird_mustacheTests.swift deleted file mode 100644 index 9202bf6..0000000 --- a/Tests/HummingbirdMustacheTests/hummingbird_mustacheTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import XCTest -@testable import hummingbird_mustache - -final class hummingbird_mustacheTests: XCTestCase { - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(hummingbird_mustache().text, "Hello, World!") - } - - static var allTests = [ - ("testExample", testExample), - ] -}