Merge pull request #4 from hummingbird-project/mustache-spec
Conform to Mustache spec
This commit is contained in:
@@ -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 ..< startIndex)
|
||||
}
|
||||
return subParser(startIndex ..< index)
|
||||
}
|
||||
|
||||
@@ -396,6 +400,16 @@ extension HBParser {
|
||||
amount -= 1
|
||||
}
|
||||
}
|
||||
|
||||
func getPosition() -> 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
|
||||
|
||||
@@ -2,37 +2,45 @@
|
||||
/// Protocol for objects that can be rendered as a sequence in Mustache
|
||||
public protocol HBMustacheSequence {
|
||||
/// Render section using template
|
||||
func renderSection(with template: HBMustacheTemplate) -> String
|
||||
func renderSection(with template: HBMustacheTemplate, stack: [Any]) -> String
|
||||
/// Render inverted section using template
|
||||
func renderInvertedSection(with template: HBMustacheTemplate) -> String
|
||||
func renderInvertedSection(with template: HBMustacheTemplate, stack: [Any]) -> String
|
||||
}
|
||||
|
||||
public extension Sequence {
|
||||
/// Render section using template
|
||||
func renderSection(with template: HBMustacheTemplate) -> String {
|
||||
func renderSection(with template: HBMustacheTemplate, stack: [Any]) -> String {
|
||||
var string = ""
|
||||
var context = HBMustacheContext(first: true)
|
||||
|
||||
var iterator = makeIterator()
|
||||
guard var currentObject = iterator.next() else { return "" }
|
||||
|
||||
while let object = iterator.next() {
|
||||
string += template.render(currentObject, context: context)
|
||||
var stack = stack
|
||||
stack.append(currentObject)
|
||||
string += template.render(stack, context: context)
|
||||
currentObject = object
|
||||
context.first = false
|
||||
context.index += 1
|
||||
}
|
||||
|
||||
context.last = true
|
||||
string += template.render(currentObject, context: context)
|
||||
var stack = stack
|
||||
stack.append(currentObject)
|
||||
string += template.render(stack, context: context)
|
||||
|
||||
return string
|
||||
}
|
||||
|
||||
/// Render inverted section using template
|
||||
func renderInvertedSection(with template: HBMustacheTemplate) -> String {
|
||||
func renderInvertedSection(with template: HBMustacheTemplate, stack: [Any]) -> String {
|
||||
var stack = stack
|
||||
stack.append(self)
|
||||
|
||||
var iterator = makeIterator()
|
||||
if iterator.next() == nil {
|
||||
return template.render(self)
|
||||
return template.render(stack)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -13,65 +13,160 @@ 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 text.count > 0 {
|
||||
tokens.append(.text(text.string))
|
||||
// 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 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 {
|
||||
parser.unsafeAdvance()
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
setNewLine = true
|
||||
if parser.current() == "\n" {
|
||||
parser.unsafeAdvance()
|
||||
}
|
||||
let sectionTokens = try parse(&parser, sectionName: name)
|
||||
} 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)))
|
||||
|
||||
case "^":
|
||||
// inverted section
|
||||
parser.unsafeAdvance()
|
||||
let (name, method) = try parseName(&parser)
|
||||
if newLine, hasLineFinished(&parser) {
|
||||
setNewLine = true
|
||||
if parser.current() == "\n" {
|
||||
parser.unsafeAdvance()
|
||||
}
|
||||
let sectionTokens = try parse(&parser, sectionName: name)
|
||||
} 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)))
|
||||
|
||||
case "/":
|
||||
// end of section
|
||||
parser.unsafeAdvance()
|
||||
let (name, _) = try parseName(&parser)
|
||||
guard name == sectionName else {
|
||||
throw Error.sectionCloseNameIncorrect
|
||||
}
|
||||
if newLine, hasLineFinished(&parser) {
|
||||
setNewLine = true
|
||||
if parser.current() == "\n" {
|
||||
parser.unsafeAdvance()
|
||||
}
|
||||
} else if whiteSpaceBefore.count > 0 {
|
||||
tokens.append(.text(whiteSpaceBefore))
|
||||
whiteSpaceBefore = ""
|
||||
}
|
||||
return tokens
|
||||
|
||||
case "!":
|
||||
// comment
|
||||
parser.unsafeAdvance()
|
||||
_ = try parseComment(&parser)
|
||||
if newLine, hasLineFinished(&parser) {
|
||||
setNewLine = true
|
||||
if !parser.reachedEnd() {
|
||||
parser.unsafeAdvance()
|
||||
}
|
||||
}
|
||||
|
||||
case "{":
|
||||
// unescaped variable
|
||||
if whiteSpaceBefore.count > 0 {
|
||||
tokens.append(.text(whiteSpaceBefore))
|
||||
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 "!":
|
||||
case "&":
|
||||
// unescaped variable
|
||||
if whiteSpaceBefore.count > 0 {
|
||||
tokens.append(.text(whiteSpaceBefore))
|
||||
whiteSpaceBefore = ""
|
||||
}
|
||||
parser.unsafeAdvance()
|
||||
_ = try parseComment(&parser)
|
||||
let (name, method) = try parseName(&parser)
|
||||
tokens.append(.unescapedVariable(name: name, method: method))
|
||||
|
||||
case ">":
|
||||
// partial
|
||||
parser.unsafeAdvance()
|
||||
let (name, _) = try parseName(&parser)
|
||||
tokens.append(.partial(name))
|
||||
if whiteSpaceBefore.count > 0 {
|
||||
tokens.append(.text(whiteSpaceBefore))
|
||||
}
|
||||
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 {
|
||||
@@ -107,6 +202,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<Unicode.Scalar>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._?")
|
||||
private static let sectionNameChars = Set<Unicode.Scalar>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._?()")
|
||||
}
|
||||
|
||||
@@ -2,60 +2,75 @@
|
||||
extension HBMustacheTemplate {
|
||||
/// Render template using object
|
||||
/// - Parameters:
|
||||
/// - object: Object
|
||||
/// - stack: Object
|
||||
/// - context: Context that render is occurring in. Contains information about position in sequence
|
||||
/// - indentation: indentation of partial
|
||||
/// - Returns: Rendered text
|
||||
func render(_ object: Any, context: HBMustacheContext? = nil) -> String {
|
||||
func render(_ stack: [Any], context: HBMustacheContext? = nil, indentation: String? = nil) -> String {
|
||||
var string = ""
|
||||
if let indentation = indentation {
|
||||
for token in tokens {
|
||||
switch token {
|
||||
case let .text(text):
|
||||
string += text
|
||||
case let .variable(variable, method):
|
||||
if let child = getChild(named: variable, from: object, method: method, context: context) {
|
||||
if let template = child as? HBMustacheTemplate {
|
||||
string += template.render(object)
|
||||
if string.last == "\n" {
|
||||
string += indentation
|
||||
}
|
||||
string += renderToken(token, stack: stack, context: context)
|
||||
}
|
||||
} else {
|
||||
string += String(describing: child).htmlEscape()
|
||||
}
|
||||
}
|
||||
case let .unescapedVariable(variable, method):
|
||||
if let child = getChild(named: variable, from: object, method: method, context: context) {
|
||||
string += String(describing: child)
|
||||
}
|
||||
case let .section(variable, method, template):
|
||||
let child = getChild(named: variable, from: object, method: method, context: context)
|
||||
string += renderSection(child, parent: object, with: template)
|
||||
|
||||
case let .invertedSection(variable, method, template):
|
||||
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
|
||||
}
|
||||
for token in tokens {
|
||||
string += renderToken(token, stack: stack, context: context)
|
||||
}
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
func renderToken(_ token: Token, stack: [Any], context: HBMustacheContext? = nil) -> String {
|
||||
switch token {
|
||||
case let .text(text):
|
||||
return text
|
||||
case let .variable(variable, method):
|
||||
if let child = getChild(named: variable, from: stack, method: method, context: context) {
|
||||
if let template = child as? HBMustacheTemplate {
|
||||
return template.render(stack)
|
||||
} else {
|
||||
return String(describing: child).htmlEscape()
|
||||
}
|
||||
}
|
||||
case let .unescapedVariable(variable, method):
|
||||
if let child = getChild(named: variable, from: stack, method: method, context: context) {
|
||||
return String(describing: child)
|
||||
}
|
||||
case let .section(variable, method, template):
|
||||
let child = getChild(named: variable, from: stack, method: method, context: context)
|
||||
return renderSection(child, stack: stack, with: template)
|
||||
|
||||
case let .invertedSection(variable, method, template):
|
||||
let child = getChild(named: variable, from: stack, method: method, context: context)
|
||||
return renderInvertedSection(child, stack: stack, with: template)
|
||||
|
||||
case let .partial(name, indentation):
|
||||
if let template = library?.getTemplate(named: name) {
|
||||
return template.render(stack, indentation: indentation)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/// Render a section
|
||||
/// - Parameters:
|
||||
/// - child: Object to render section for
|
||||
/// - parent: Current object being rendered
|
||||
/// - template: Template to render with
|
||||
/// - Returns: Rendered text
|
||||
func renderSection(_ child: Any?, parent: Any, with template: HBMustacheTemplate) -> String {
|
||||
func renderSection(_ child: Any?, stack: [Any], with template: HBMustacheTemplate) -> String {
|
||||
switch child {
|
||||
case let array as HBMustacheSequence:
|
||||
return array.renderSection(with: template)
|
||||
return array.renderSection(with: template, stack: stack + [array])
|
||||
case let bool as Bool:
|
||||
return bool ? template.render(parent) : ""
|
||||
return bool ? template.render(stack) : ""
|
||||
case let lambda as HBMustacheLambda:
|
||||
return lambda.run(parent, template)
|
||||
return lambda.run(stack.last!, template)
|
||||
case let .some(value):
|
||||
return template.render(value)
|
||||
return template.render(stack + [value])
|
||||
case .none:
|
||||
return ""
|
||||
}
|
||||
@@ -67,33 +82,46 @@ extension HBMustacheTemplate {
|
||||
/// - parent: Current object being rendered
|
||||
/// - template: Template to render with
|
||||
/// - Returns: Rendered text
|
||||
func renderInvertedSection(_ child: Any?, parent: Any, with template: HBMustacheTemplate) -> String {
|
||||
func renderInvertedSection(_ child: Any?, stack: [Any], with template: HBMustacheTemplate) -> String {
|
||||
switch child {
|
||||
case let array as HBMustacheSequence:
|
||||
return array.renderInvertedSection(with: template)
|
||||
return array.renderInvertedSection(with: template, stack: stack)
|
||||
case let bool as Bool:
|
||||
return bool ? "" : template.render(parent)
|
||||
return bool ? "" : template.render(stack)
|
||||
case .some:
|
||||
return ""
|
||||
case .none:
|
||||
return template.render(parent)
|
||||
return template.render(stack)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get child object from variable name
|
||||
func getChild(named name: String, from object: Any, method: String?, context: HBMustacheContext?) -> Any? {
|
||||
func _getChild(named names: ArraySlice<String>, from object: Any) -> Any? {
|
||||
guard let name = names.first else { return object }
|
||||
let childObject: Any?
|
||||
func getChild(named name: String, from stack: [Any], method: String?, context: HBMustacheContext?) -> Any? {
|
||||
func _getImmediateChild(named name: String, from object: Any) -> Any? {
|
||||
if let customBox = object as? HBMustacheParent {
|
||||
childObject = customBox.child(named: name)
|
||||
return customBox.child(named: name)
|
||||
} else {
|
||||
let mirror = Mirror(reflecting: object)
|
||||
childObject = mirror.getValue(forKey: name)
|
||||
return mirror.getValue(forKey: name)
|
||||
}
|
||||
guard childObject != nil else { return nil }
|
||||
}
|
||||
|
||||
func _getChild(named names: ArraySlice<String>, from object: Any) -> Any? {
|
||||
guard let name = names.first else { return object }
|
||||
guard let childObject = _getImmediateChild(named: name, from: object) else { return nil }
|
||||
let names2 = names.dropFirst()
|
||||
return _getChild(named: names2, from: childObject!)
|
||||
return _getChild(named: names2, from: childObject)
|
||||
}
|
||||
|
||||
func _getChildInStack(named names: ArraySlice<String>, from stack: [Any]) -> Any? {
|
||||
guard let name = names.first else { return stack.last }
|
||||
for object in stack.reversed() {
|
||||
if let childObject = _getImmediateChild(named: name, from: object) {
|
||||
let names2 = names.dropFirst()
|
||||
return _getChild(named: names2, from: childObject)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// work out which object to access. "." means the current object, if the variable name is ""
|
||||
@@ -101,12 +129,12 @@ extension HBMustacheTemplate {
|
||||
// the name is split by "." and we use mirror to get the correct child object
|
||||
let child: Any?
|
||||
if name == "." {
|
||||
child = object
|
||||
child = stack.last!
|
||||
} else if name == "", method != nil {
|
||||
child = context
|
||||
} else {
|
||||
let nameSplit = name.split(separator: ".").map { String($0) }
|
||||
child = _getChild(named: nameSplit[...], from: object)
|
||||
child = _getChildInStack(named: nameSplit[...], from: stack)
|
||||
}
|
||||
// if we want to run a method and the current child can have methods applied to it then
|
||||
// run method on the current child
|
||||
|
||||
@@ -11,7 +11,7 @@ public final class HBMustacheTemplate {
|
||||
/// - Parameter object: Object to render
|
||||
/// - Returns: Rendered text
|
||||
public func render(_ object: Any) -> String {
|
||||
render(object, context: nil)
|
||||
render([object], context: nil)
|
||||
}
|
||||
|
||||
internal init(_ tokens: [Token]) {
|
||||
@@ -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]
|
||||
|
||||
@@ -18,11 +18,28 @@ final class MethodTests: XCTestCase {
|
||||
XCTAssertEqual(template.render(object), "TEST")
|
||||
}
|
||||
|
||||
func testNewline() 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>
|
||||
|
||||
""")
|
||||
}
|
||||
|
||||
func testFirstLast() throws {
|
||||
let template = try HBMustacheTemplate(string: """
|
||||
{{#repo}}
|
||||
<b>{{#first()}}first: {{/}}{{#last()}}last: {{/}}{{ name }}</b>
|
||||
{{/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}}
|
||||
<b>{{#index()}}{{plusone(.)}}{{/}}) {{ name }}</b>
|
||||
{{/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}}
|
||||
<b>{{index()}}) {{#even()}}even {{/}}{{#odd()}}odd {{/}}{{ name }}</b>
|
||||
{{/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)}}
|
||||
<b>{{ name }}</b>
|
||||
{{/repo}}
|
||||
|
||||
""")
|
||||
let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]]
|
||||
XCTAssertEqual(template.render(object), """
|
||||
|
||||
@@ -13,6 +13,7 @@ final class PartialTests: XCTestCase {
|
||||
""")
|
||||
let template2 = try HBMustacheTemplate(string: """
|
||||
<strong>{{.}}</strong>
|
||||
|
||||
""")
|
||||
library.register(template, named: "base")
|
||||
library.register(template2, named: "user")
|
||||
|
||||
906
Tests/HummingbirdMustacheTests/SpecTests.swift
Normal file
906
Tests/HummingbirdMustacheTests/SpecTests.swift
Normal file
@@ -0,0 +1,906 @@
|
||||
import HummingbirdMustache
|
||||
import XCTest
|
||||
|
||||
/// Mustache spec tests. These are the formal standard for Mustache. More details
|
||||
/// can be found at https://github.com/mustache/spec
|
||||
|
||||
func test(_ object: Any, _ template: String, _ expected: String) throws {
|
||||
let template = try HBMustacheTemplate(string: template)
|
||||
let result = template.render(object)
|
||||
XCTAssertEqual(result, expected)
|
||||
}
|
||||
|
||||
// MARK: Comments
|
||||
|
||||
final class SpecCommentsTests: XCTestCase {
|
||||
func testInline() throws {
|
||||
let object = {}
|
||||
let template = "12345{{! Comment Block! }}67890"
|
||||
let expected = "1234567890"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testMultiline() throws {
|
||||
let object = {}
|
||||
let template = """
|
||||
12345{{!
|
||||
This is a
|
||||
multi-line comment...
|
||||
}}67890
|
||||
"""
|
||||
let expected = "1234567890"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testStandalone() throws {
|
||||
let object = {}
|
||||
let template = """
|
||||
Begin.
|
||||
{{! Comment Block! }}
|
||||
End.
|
||||
"""
|
||||
let expected = """
|
||||
Begin.
|
||||
End.
|
||||
"""
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testIndentedStandalone() throws {
|
||||
let object = {}
|
||||
let template = """
|
||||
Begin.
|
||||
{{! Comment Block! }}
|
||||
End.
|
||||
"""
|
||||
let expected = """
|
||||
Begin.
|
||||
End.
|
||||
"""
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testStandaloneLineEndings() throws {
|
||||
let object = {}
|
||||
let template = "\r\n{{! Standalone Comment }}\r\n"
|
||||
let expected = "\r\n"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testStandaloneWithoutPreviousLine() throws {
|
||||
let object = {}
|
||||
let template = " {{! I'm Still Standalone }}\n!"
|
||||
let expected = "!"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testStandaloneWithoutNewLine() throws {
|
||||
let object = {}
|
||||
let template = "!\n {{! I'm Still Standalone }}"
|
||||
let expected = "!\n"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testStandaloneMultiLine() throws {
|
||||
let object = {}
|
||||
let template = """
|
||||
Begin.
|
||||
{{!
|
||||
Something's going on here...
|
||||
}}
|
||||
End.
|
||||
"""
|
||||
let expected = """
|
||||
Begin.
|
||||
End.
|
||||
"""
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testIndentedInline() throws {
|
||||
let object = {}
|
||||
let template = " 12 {{! 34 }}\n"
|
||||
let expected = " 12 \n"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testSurroundingWhitespace() throws {
|
||||
let object = {}
|
||||
let template = "12345 {{! Comment Block! }} 67890"
|
||||
let expected = "12345 67890"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Interpolation
|
||||
|
||||
final class SpecInterpolationTests: XCTestCase {
|
||||
func testNoInterpolation() throws {
|
||||
let object = {}
|
||||
let template = "Hello from {Mustache}!"
|
||||
let expected = "Hello from {Mustache}!"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testBasicInterpolation() throws {
|
||||
let object = ["subject": "world"]
|
||||
let template = "Hello, {{subject}}!"
|
||||
let expected = "Hello, world!"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testHTMLEscaping() throws {
|
||||
let object = ["forbidden": #"& " < >"#]
|
||||
let template = "These characters should be HTML escaped: {{forbidden}}"
|
||||
let expected = #"These characters should be HTML escaped: & " < >"#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testTripleMustache() throws {
|
||||
let object = ["forbidden": #"& " < >"#]
|
||||
let template = "These characters should not be HTML escaped: {{{forbidden}}}"
|
||||
let expected = #"These characters should not be HTML escaped: & " < >"#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testAmpersand() throws {
|
||||
let object = ["forbidden": #"& " < >"#]
|
||||
let template = "These characters should not be HTML escaped: {{&forbidden}}"
|
||||
let expected = #"These characters should not be HTML escaped: & " < >"#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testBasicInteger() throws {
|
||||
let object = ["mph": 85]
|
||||
let template = #""{{mph}} miles an hour!""#
|
||||
let expected = #""85 miles an hour!""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testTripleMustacheInteger() throws {
|
||||
let object = ["mph": 85]
|
||||
let template = #""{{{mph}}} miles an hour!""#
|
||||
let expected = #""85 miles an hour!""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testBasicDecimal() throws {
|
||||
let object = ["power": 1.210]
|
||||
let template = #""{{power}} jiggawatts!""#
|
||||
let expected = #""1.21 jiggawatts!""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testTripleMustacheDecimal() throws {
|
||||
let object = ["power": 1.210]
|
||||
let template = #""{{{power}}} jiggawatts!""#
|
||||
let expected = #""1.21 jiggawatts!""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testAmpersandDecimal() throws {
|
||||
let object = ["power": 1.210]
|
||||
let template = #""{{&power}} jiggawatts!""#
|
||||
let expected = #""1.21 jiggawatts!""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testContextMiss() throws {
|
||||
let object = {}
|
||||
let template = #"I ({{cannot}}) be seen!"#
|
||||
let expected = #"I () be seen!"#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testTripleMustacheContextMiss() throws {
|
||||
let object = {}
|
||||
let template = #"I ({{{cannot}}}) be seen!"#
|
||||
let expected = #"I () be seen!"#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testAmpersandContextMiss() throws {
|
||||
let object = {}
|
||||
let template = #"I ({{&cannot}}) be seen!"#
|
||||
let expected = #"I () be seen!"#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testDottedName() throws {
|
||||
let object = ["person": ["name": "Joe"]]
|
||||
let template = #""{{person.name}}" == "{{#person}}{{name}}{{/person}}""#
|
||||
let expected = #""Joe" == "Joe""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testTripleMustacheDottedName() throws {
|
||||
let object = ["person": ["name": "Joe"]]
|
||||
let template = #""{{{person.name}}}" == "{{#person}}{{name}}{{/person}}""#
|
||||
let expected = #""Joe" == "Joe""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testAmpersandDottedName() throws {
|
||||
let object = ["person": ["name": "Joe"]]
|
||||
let template = #""{{&person.name}}" == "{{#person}}{{name}}{{/person}}""#
|
||||
let expected = #""Joe" == "Joe""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testArbituaryDepthDottedName() throws {
|
||||
let object = ["a": ["b": ["c": ["d": ["e": ["name": "Phil"]]]]]]
|
||||
let template = #""{{a.b.c.d.e.name}}" == "Phil""#
|
||||
let expected = #""Phil" == "Phil""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testBrokenChainDottedName() throws {
|
||||
let object = ["a": ["b": []], "c": ["name": "Jim"]]
|
||||
let template = #""{{a.b.c.name}}" == """#
|
||||
let expected = "\"\" == \"\""
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testInitialResolutionDottedName() throws {
|
||||
let object = [
|
||||
"a": ["b": ["c": ["d": ["e": ["name": "Phil"]]]]],
|
||||
"b": ["c": ["d": ["e": ["name": "Wrong"]]]],
|
||||
]
|
||||
let template = #""{{#a}}{{b.c.d.e.name}}{{/a}}" == "Phil""#
|
||||
let expected = #""Phil" == "Phil""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testContextPrecedenceDottedName() throws {
|
||||
let object = [
|
||||
"a": ["b": []],
|
||||
"b": ["c": "Error"],
|
||||
]
|
||||
let template = #"{{#a}}{{b.c}}{{/a}}"#
|
||||
let expected = ""
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testSurroundingWhitespace() throws {
|
||||
let object = ["string": "---"]
|
||||
let template = "| {{string}} |"
|
||||
let expected = "| --- |"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testTripleMustacheSurroundingWhitespace() throws {
|
||||
let object = ["string": "---"]
|
||||
let template = "| {{{string}}} |"
|
||||
let expected = "| --- |"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testAmpersandSurroundingWhitespace() throws {
|
||||
let object = ["string": "---"]
|
||||
let template = "| {{&string}} |"
|
||||
let expected = "| --- |"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testInterpolationStandalone() throws {
|
||||
let object = ["string": "---"]
|
||||
let template = " {{string}}\n"
|
||||
let expected = " ---\n"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testTripleMustacheStandalone() throws {
|
||||
let object = ["string": "---"]
|
||||
let template = " {{{string}}}\n"
|
||||
let expected = " ---\n"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testAmpersandStandalone() throws {
|
||||
let object = ["string": "---"]
|
||||
let template = " {{&string}}\n"
|
||||
let expected = " ---\n"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testInterpolationWithPadding() throws {
|
||||
let object = ["string": "---"]
|
||||
let template = "|{{ string }}|"
|
||||
let expected = "|---|"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testTripleMustacheWithPadding() throws {
|
||||
let object = ["string": "---"]
|
||||
let template = "|{{{ string }}}|"
|
||||
let expected = "|---|"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testAmpersandWithPadding() throws {
|
||||
let object = ["string": "---"]
|
||||
let template = "|{{& string }}|"
|
||||
let expected = "|---|"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Inverted
|
||||
|
||||
final class SpecInvertedTests: XCTestCase {
|
||||
func testFalse() throws {
|
||||
let object = ["boolean": false]
|
||||
let template = #""{{^boolean}}This should be rendered.{{/boolean}}""#
|
||||
let expected = #""This should be rendered.""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testTrue() throws {
|
||||
let object = ["boolean": true]
|
||||
let template = #""{{^boolean}}This should not be rendered.{{/boolean}}""#
|
||||
let expected = "\"\""
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testContext() throws {
|
||||
let object = ["context": ["name": "Joe"]]
|
||||
let template = #""{{^context}}Hi {{name}}.{{/context}}""#
|
||||
let expected = "\"\""
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testList() throws {
|
||||
let object = ["list": [["n": 1], ["n": 2], ["n": 3]]]
|
||||
let template = #""{{^list}}{{n}}{{/list}}""#
|
||||
let expected = "\"\""
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testEmptyList() throws {
|
||||
let object = ["list": []]
|
||||
let template = #""{{^list}}Yay lists!{{/list}}""#
|
||||
let expected = #""Yay lists!""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testDoubled() throws {
|
||||
let object: [String: Any] = ["bool": false, "two": "second"]
|
||||
let template = """
|
||||
{{^bool}}
|
||||
* first
|
||||
{{/bool}}
|
||||
* {{two}}
|
||||
{{^bool}}
|
||||
* third
|
||||
{{/bool}}
|
||||
"""
|
||||
let expected = """
|
||||
* first
|
||||
* second
|
||||
* third
|
||||
|
||||
"""
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testNestedFalse() throws {
|
||||
let object = ["bool": false]
|
||||
let template = #"| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |"#
|
||||
let expected = #"| A B C D E |"#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testNestedTrue() throws {
|
||||
let object = ["bool": true]
|
||||
let template = #"| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |"#
|
||||
let expected = #"| A E |"#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testContextMiss() throws {
|
||||
let object = {}
|
||||
let template = #"[{{^missing}}Cannot find key 'missing'!{{/missing}}]"#
|
||||
let expected = #"[Cannot find key 'missing'!]"#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testDottedNamesTrue() throws {
|
||||
let object = ["a": ["b": ["c": true]]]
|
||||
let template = #""{{^a.b.c}}Not Here{{/a.b.c}}" == """#
|
||||
let expected = "\"\" == \"\""
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testDottedNamesFalse() throws {
|
||||
let object = ["a": ["b": ["c": false]]]
|
||||
let template = #""{{^a.b.c}}Not Here{{/a.b.c}}" == "Not Here""#
|
||||
let expected = #""Not Here" == "Not Here""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testDottedNamesBrokenChain() throws {
|
||||
let object = ["a": {}]
|
||||
let template = #""{{^a.b.c}}Not Here{{/a.b.c}}" == "Not Here""#
|
||||
let expected = #""Not Here" == "Not Here""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testSurroundingWhitespace() throws {
|
||||
let object = ["boolean": false]
|
||||
let template = " | {{^boolean}}\t|\t{{/boolean}} | \n"
|
||||
let expected = " | \t|\t | \n"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testInternalWhitespace() throws {
|
||||
let object = ["boolean": false]
|
||||
let template = " | {{^boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n"
|
||||
let expected = " | \n | \n"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testIndentedInline() throws {
|
||||
let object = ["boolean": false]
|
||||
let template = " {{^boolean}}NO{{/boolean}}\n {{^boolean}}WAY{{/boolean}}\n"
|
||||
let expected = " NO\n WAY\n"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testStandaloneLines() throws {
|
||||
let object = ["boolean": false]
|
||||
let template = """
|
||||
| This Is
|
||||
{{^boolean}}
|
||||
|
|
||||
{{/boolean}}
|
||||
| A Line
|
||||
"""
|
||||
let expected = """
|
||||
| This Is
|
||||
|
|
||||
| A Line
|
||||
"""
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testStandaloneIndentedLines() throws {
|
||||
let object = ["boolean": false]
|
||||
let template = """
|
||||
| This Is
|
||||
{{^boolean}}
|
||||
|
|
||||
{{/boolean}}
|
||||
| A Line
|
||||
"""
|
||||
let expected = """
|
||||
| This Is
|
||||
|
|
||||
| A Line
|
||||
"""
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testStandaloneLineEndings() throws {
|
||||
let object = ["boolean": false]
|
||||
let template = "|\r\n{{^boolean}}\r\n{{/boolean}}\r\n|"
|
||||
let expected = "|\r\n|"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testStandaloneWithoutPreviousLine() throws {
|
||||
let object = ["boolean": false]
|
||||
let template = " {{^boolean}}\n^{{/boolean}}\n/"
|
||||
let expected = "^\n/"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testStandaloneWithoutNewLine() throws {
|
||||
let object = ["boolean": false]
|
||||
let template = "^{{^boolean}}\n/\n {{/boolean}}"
|
||||
let expected = "^\n/\n"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testPadding() throws {
|
||||
let object = ["boolean": false]
|
||||
let template = "|{{^ boolean }}={{/ boolean }}|"
|
||||
let expected = "|=|"
|
||||
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<Y<>>"#
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Sections
|
||||
|
||||
final class SpecSectionTests: XCTestCase {
|
||||
func testTrue() throws {
|
||||
let object = ["boolean": true]
|
||||
let template = #""{{#boolean}}This should be rendered.{{/boolean}}""#
|
||||
let expected = #""This should be rendered.""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testFalse() throws {
|
||||
let object = ["boolean": false]
|
||||
let template = #""{{#boolean}}This should not be rendered.{{/boolean}}""#
|
||||
let expected = "\"\""
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testContext() throws {
|
||||
let object = ["context": ["name": "Joe"]]
|
||||
let template = #""{{#context}}Hi {{name}}.{{/context}}""#
|
||||
let expected = #""Hi Joe.""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testParentContext() throws {
|
||||
let object: [String: Any] = ["a": "foo", "b": "wrong", "sec": ["b": "bar"], "c": ["d": "baz"]]
|
||||
let template = #""{{#sec}}{{a}}, {{b}}, {{c.d}}{{/sec}}""#
|
||||
let expected = #""foo, bar, baz""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testVariables() throws {
|
||||
let object: [String: Any] = ["foo": "bar"]
|
||||
let template = #""{{#foo}}{{.}} is {{foo}}{{/foo}}""#
|
||||
let expected = #""bar is bar""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testListContexts() throws {
|
||||
let object: [String: Any] = ["tops": ["tname": ["upper": "A", "lower": "a"], "middles": ["mname": "1", "bottoms": [["bname": "x"], ["bname": "y"]]]]]
|
||||
let template = #"{{#tops}}{{#middles}}{{tname.lower}}{{mname}}.{{#bottoms}}{{tname.upper}}{{mname}}{{bname}}.{{/bottoms}}{{/middles}}{{/tops}}"#
|
||||
let expected = #"a1.A1x.A1y."#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testDeeplyNestedContexts() throws {
|
||||
let object: [String: Any] = ["a": ["one": 1], "b": ["two": 2], "c": ["three": 3, "d": ["four": 4, "five": 5]]]
|
||||
let template = """
|
||||
{{#a}}
|
||||
{{one}}
|
||||
{{#b}}
|
||||
{{one}}{{two}}{{one}}
|
||||
{{#c}}
|
||||
{{one}}{{two}}{{three}}{{two}}{{one}}
|
||||
{{#d}}
|
||||
{{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}
|
||||
{{#five}}
|
||||
{{one}}{{two}}{{three}}{{four}}{{five}}{{four}}{{three}}{{two}}{{one}}
|
||||
{{one}}{{two}}{{three}}{{four}}{{.}}6{{.}}{{four}}{{three}}{{two}}{{one}}
|
||||
{{one}}{{two}}{{three}}{{four}}{{five}}{{four}}{{three}}{{two}}{{one}}
|
||||
{{/five}}
|
||||
{{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}
|
||||
{{/d}}
|
||||
{{one}}{{two}}{{three}}{{two}}{{one}}
|
||||
{{/c}}
|
||||
{{one}}{{two}}{{one}}
|
||||
{{/b}}
|
||||
{{one}}
|
||||
{{/a}}
|
||||
|
||||
"""
|
||||
let expected = """
|
||||
1
|
||||
121
|
||||
12321
|
||||
1234321
|
||||
123454321
|
||||
12345654321
|
||||
123454321
|
||||
1234321
|
||||
12321
|
||||
121
|
||||
1
|
||||
|
||||
"""
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testList() throws {
|
||||
let object: [String: Any] = ["list": [["item": 1], ["item": 2], ["item": 3]]]
|
||||
let template = #""{{#list}}{{item}}{{/list}}""#
|
||||
let expected = #""123""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testEmptyList() throws {
|
||||
let object: [Any] = []
|
||||
let template = #""{{#list}}Yay lists!{{/list}}""#
|
||||
let expected = "\"\""
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testDoubled() throws {
|
||||
let object: [String: Any] = ["bool": true, "two": "second"]
|
||||
let template = """
|
||||
{{#bool}}
|
||||
* first
|
||||
{{/bool}}
|
||||
* {{two}}
|
||||
{{#bool}}
|
||||
* third
|
||||
{{/bool}}
|
||||
|
||||
"""
|
||||
let expected = """
|
||||
* first
|
||||
* second
|
||||
* third
|
||||
|
||||
"""
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testNestedTrue() throws {
|
||||
let object = ["bool": true]
|
||||
let template = "| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |"
|
||||
let expected = "| A B C D E |"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testNestedFalse() throws {
|
||||
let object = ["bool": false]
|
||||
let template = "| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |"
|
||||
let expected = "| A E |"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testContextMiss() throws {
|
||||
let object = {}
|
||||
let template = "[{{#missing}}Found key 'missing'!{{/missing}}]"
|
||||
let expected = "[]"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testImplicitIteratorString() throws {
|
||||
let object = ["list": ["a", "b", "c", "d", "e"]]
|
||||
let template = #""{{#list}}({{.}}){{/list}}""#
|
||||
let expected = #""(a)(b)(c)(d)(e)""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testImplicitIteratorInteger() throws {
|
||||
let object = ["list": [1, 2, 3, 4, 5]]
|
||||
let template = #""{{#list}}({{.}}){{/list}}""#
|
||||
let expected = #""(1)(2)(3)(4)(5)""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testImplicitIteratorDecimal() throws {
|
||||
let object = ["list": [1.1, 2.2, 3.3, 4.4, 5.5]]
|
||||
let template = #""{{#list}}({{.}}){{/list}}""#
|
||||
let expected = #""(1.1)(2.2)(3.3)(4.4)(5.5)""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testImplicitIteratorArray() throws {
|
||||
let object: [String: Any] = ["list": [[1, 2, 3], ["a", "b", "c"]]]
|
||||
let template = #""{{#list}}({{#.}}{{.}}{{/.}}){{/list}}""#
|
||||
let expected = #""(123)(abc)""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testDottedNameTrue() throws {
|
||||
let object = ["a": ["b": ["c": true]]]
|
||||
let template = #""{{#a.b.c}}Here{{/a.b.c}}" == "Here""#
|
||||
let expected = #""Here" == "Here""#
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testDottedNameFalse() throws {
|
||||
let object = ["a": ["b": ["c": false]]]
|
||||
let template = #""{{#a.b.c}}Here{{/a.b.c}}" == """#
|
||||
let expected = "\"\" == \"\""
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testDottedNameBrokenChain() throws {
|
||||
let object = ["a": []]
|
||||
let template = #""{{#a.b.c}}Here{{/a.b.c}}" == """#
|
||||
let expected = "\"\" == \"\""
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testSurroundingWhitespace() throws {
|
||||
let object = ["boolean": true]
|
||||
let template = " | {{#boolean}}\t|\t{{/boolean}} | \n"
|
||||
let expected = " | \t|\t | \n"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testInternalWhitespace() throws {
|
||||
let object = ["boolean": true]
|
||||
let template = " | {{#boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n"
|
||||
let expected = " | \n | \n"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testIndentedInline() throws {
|
||||
let object = ["boolean": true]
|
||||
let template = " {{#boolean}}YES{{/boolean}}\n {{#boolean}}GOOD{{/boolean}}\n"
|
||||
let expected = " YES\n GOOD\n"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testStandaloneLines() throws {
|
||||
let object = ["boolean": true]
|
||||
let template = """
|
||||
| This Is
|
||||
{{#boolean}}
|
||||
|
|
||||
{{/boolean}}
|
||||
| A Line
|
||||
"""
|
||||
let expected = """
|
||||
| This Is
|
||||
|
|
||||
| A Line
|
||||
"""
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testIndentedStandaloneLines() throws {
|
||||
let object = ["boolean": true]
|
||||
let template = """
|
||||
| This Is
|
||||
{{#boolean}}
|
||||
|
|
||||
{{/boolean}}
|
||||
| A Line
|
||||
"""
|
||||
let expected = """
|
||||
| This Is
|
||||
|
|
||||
| A Line
|
||||
"""
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testStandaloneLineEndings() throws {
|
||||
let object = ["boolean": true]
|
||||
let template = "|\r\n{{#boolean}}\r\n{{/boolean}}\r\n|"
|
||||
let expected = "|\r\n|"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testStandaloneWithoutPreviousLine() throws {
|
||||
let object = ["boolean": true]
|
||||
let template = " {{#boolean}}\n#{{/boolean}}\n/"
|
||||
let expected = "#\n/"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testStandaloneWithoutNewLine() throws {
|
||||
let object = ["boolean": true]
|
||||
let template = "#{{#boolean}}\n/\n {{/boolean}}"
|
||||
let expected = "#\n/\n"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
|
||||
func testPadding() throws {
|
||||
let object = ["boolean": true]
|
||||
let template = "|{{# boolean }}={{/ boolean }}|"
|
||||
let expected = "|=|"
|
||||
try test(object, template, expected)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user