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 unexpected
|
||||||
case emptyString
|
case emptyString
|
||||||
case invalidUTF8
|
case invalidUTF8
|
||||||
|
case invalidPosition
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a Parser object
|
/// Create a Parser object
|
||||||
@@ -282,6 +283,9 @@ extension HBParser {
|
|||||||
{
|
{
|
||||||
unsafeAdvance()
|
unsafeAdvance()
|
||||||
}
|
}
|
||||||
|
if startIndex == index {
|
||||||
|
return subParser(startIndex ..< startIndex)
|
||||||
|
}
|
||||||
return subParser(startIndex ..< index)
|
return subParser(startIndex ..< index)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,6 +400,16 @@ extension HBParser {
|
|||||||
amount -= 1
|
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
|
/// extend Parser to conform to Sequence
|
||||||
|
|||||||
@@ -2,37 +2,45 @@
|
|||||||
/// Protocol for objects that can be rendered as a sequence in Mustache
|
/// Protocol for objects that can be rendered as a sequence in Mustache
|
||||||
public protocol HBMustacheSequence {
|
public protocol HBMustacheSequence {
|
||||||
/// Render section using template
|
/// Render section using template
|
||||||
func renderSection(with template: HBMustacheTemplate) -> String
|
func renderSection(with template: HBMustacheTemplate, stack: [Any]) -> String
|
||||||
/// Render inverted section using template
|
/// Render inverted section using template
|
||||||
func renderInvertedSection(with template: HBMustacheTemplate) -> String
|
func renderInvertedSection(with template: HBMustacheTemplate, stack: [Any]) -> String
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Sequence {
|
public extension Sequence {
|
||||||
/// Render section using template
|
/// Render section using template
|
||||||
func renderSection(with template: HBMustacheTemplate) -> String {
|
func renderSection(with template: HBMustacheTemplate, stack: [Any]) -> String {
|
||||||
var string = ""
|
var string = ""
|
||||||
var context = HBMustacheContext(first: true)
|
var context = HBMustacheContext(first: true)
|
||||||
|
|
||||||
var iterator = makeIterator()
|
var iterator = makeIterator()
|
||||||
guard var currentObject = iterator.next() else { return "" }
|
guard var currentObject = iterator.next() else { return "" }
|
||||||
|
|
||||||
while let object = iterator.next() {
|
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
|
currentObject = object
|
||||||
context.first = false
|
context.first = false
|
||||||
context.index += 1
|
context.index += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
context.last = true
|
context.last = true
|
||||||
string += template.render(currentObject, context: context)
|
var stack = stack
|
||||||
|
stack.append(currentObject)
|
||||||
|
string += template.render(stack, context: context)
|
||||||
|
|
||||||
return string
|
return string
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render inverted section using template
|
/// 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()
|
var iterator = makeIterator()
|
||||||
if iterator.next() == nil {
|
if iterator.next() == nil {
|
||||||
return template.render(self)
|
return template.render(stack)
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,65 +13,160 @@ extension HBMustacheTemplate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// parse section in mustache text
|
/// 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 tokens: [Token] = []
|
||||||
|
var newLine = newLine
|
||||||
|
var whiteSpaceBefore: String = ""
|
||||||
while !parser.reachedEnd() {
|
while !parser.reachedEnd() {
|
||||||
let text = try parser.read(untilString: "{{", throwOnOverflow: false, skipToEnd: true)
|
// if new line read whitespace
|
||||||
if text.count > 0 {
|
if newLine {
|
||||||
tokens.append(.text(text.string))
|
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() {
|
if parser.reachedEnd() {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
var setNewLine = false
|
||||||
switch parser.current() {
|
switch parser.current() {
|
||||||
case "#":
|
case "#":
|
||||||
|
// section
|
||||||
parser.unsafeAdvance()
|
parser.unsafeAdvance()
|
||||||
let (name, method) = try parseName(&parser)
|
let (name, method) = try parseName(&parser)
|
||||||
|
if newLine, hasLineFinished(&parser) {
|
||||||
|
setNewLine = true
|
||||||
if parser.current() == "\n" {
|
if parser.current() == "\n" {
|
||||||
parser.unsafeAdvance()
|
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)))
|
tokens.append(.section(name: name, method: method, template: HBMustacheTemplate(sectionTokens)))
|
||||||
|
|
||||||
case "^":
|
case "^":
|
||||||
|
// inverted section
|
||||||
parser.unsafeAdvance()
|
parser.unsafeAdvance()
|
||||||
let (name, method) = try parseName(&parser)
|
let (name, method) = try parseName(&parser)
|
||||||
|
if newLine, hasLineFinished(&parser) {
|
||||||
|
setNewLine = true
|
||||||
if parser.current() == "\n" {
|
if parser.current() == "\n" {
|
||||||
parser.unsafeAdvance()
|
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)))
|
tokens.append(.invertedSection(name: name, method: method, template: HBMustacheTemplate(sectionTokens)))
|
||||||
|
|
||||||
case "/":
|
case "/":
|
||||||
|
// end of section
|
||||||
parser.unsafeAdvance()
|
parser.unsafeAdvance()
|
||||||
let (name, _) = try parseName(&parser)
|
let (name, _) = try parseName(&parser)
|
||||||
guard name == sectionName else {
|
guard name == sectionName else {
|
||||||
throw Error.sectionCloseNameIncorrect
|
throw Error.sectionCloseNameIncorrect
|
||||||
}
|
}
|
||||||
|
if newLine, hasLineFinished(&parser) {
|
||||||
|
setNewLine = true
|
||||||
if parser.current() == "\n" {
|
if parser.current() == "\n" {
|
||||||
parser.unsafeAdvance()
|
parser.unsafeAdvance()
|
||||||
}
|
}
|
||||||
|
} else if whiteSpaceBefore.count > 0 {
|
||||||
|
tokens.append(.text(whiteSpaceBefore))
|
||||||
|
whiteSpaceBefore = ""
|
||||||
|
}
|
||||||
return tokens
|
return tokens
|
||||||
|
|
||||||
|
case "!":
|
||||||
|
// comment
|
||||||
|
parser.unsafeAdvance()
|
||||||
|
_ = try parseComment(&parser)
|
||||||
|
if newLine, hasLineFinished(&parser) {
|
||||||
|
setNewLine = true
|
||||||
|
if !parser.reachedEnd() {
|
||||||
|
parser.unsafeAdvance()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case "{":
|
case "{":
|
||||||
|
// unescaped variable
|
||||||
|
if whiteSpaceBefore.count > 0 {
|
||||||
|
tokens.append(.text(whiteSpaceBefore))
|
||||||
|
whiteSpaceBefore = ""
|
||||||
|
}
|
||||||
parser.unsafeAdvance()
|
parser.unsafeAdvance()
|
||||||
let (name, method) = try parseName(&parser)
|
let (name, method) = try parseName(&parser)
|
||||||
guard try parser.read("}") else { throw Error.unfinishedName }
|
guard try parser.read("}") else { throw Error.unfinishedName }
|
||||||
tokens.append(.unescapedVariable(name: name, method: method))
|
tokens.append(.unescapedVariable(name: name, method: method))
|
||||||
|
|
||||||
case "!":
|
case "&":
|
||||||
|
// unescaped variable
|
||||||
|
if whiteSpaceBefore.count > 0 {
|
||||||
|
tokens.append(.text(whiteSpaceBefore))
|
||||||
|
whiteSpaceBefore = ""
|
||||||
|
}
|
||||||
parser.unsafeAdvance()
|
parser.unsafeAdvance()
|
||||||
_ = try parseComment(&parser)
|
let (name, method) = try parseName(&parser)
|
||||||
|
tokens.append(.unescapedVariable(name: name, method: method))
|
||||||
|
|
||||||
case ">":
|
case ">":
|
||||||
|
// partial
|
||||||
parser.unsafeAdvance()
|
parser.unsafeAdvance()
|
||||||
let (name, _) = try parseName(&parser)
|
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:
|
default:
|
||||||
|
// variable
|
||||||
|
if whiteSpaceBefore.count > 0 {
|
||||||
|
tokens.append(.text(whiteSpaceBefore))
|
||||||
|
whiteSpaceBefore = ""
|
||||||
|
}
|
||||||
let (name, method) = try parseName(&parser)
|
let (name, method) = try parseName(&parser)
|
||||||
tokens.append(.variable(name: name, method: method))
|
tokens.append(.variable(name: name, method: method))
|
||||||
}
|
}
|
||||||
|
newLine = setNewLine
|
||||||
}
|
}
|
||||||
// should never get here if reading section
|
// should never get here if reading section
|
||||||
guard sectionName == nil else {
|
guard sectionName == nil else {
|
||||||
@@ -107,6 +202,17 @@ extension HBMustacheTemplate {
|
|||||||
return text.string
|
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 sectionNameCharsWithoutBrackets = Set<Unicode.Scalar>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._?")
|
||||||
private static let sectionNameChars = Set<Unicode.Scalar>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._?()")
|
private static let sectionNameChars = Set<Unicode.Scalar>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._?()")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,60 +2,75 @@
|
|||||||
extension HBMustacheTemplate {
|
extension HBMustacheTemplate {
|
||||||
/// Render template using object
|
/// Render template using object
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - object: Object
|
/// - stack: Object
|
||||||
/// - context: Context that render is occurring in. Contains information about position in sequence
|
/// - context: Context that render is occurring in. Contains information about position in sequence
|
||||||
|
/// - indentation: indentation of partial
|
||||||
/// - Returns: Rendered text
|
/// - Returns: Rendered text
|
||||||
func render(_ object: Any, context: HBMustacheContext? = nil) -> String {
|
func render(_ stack: [Any], context: HBMustacheContext? = nil, indentation: String? = nil) -> String {
|
||||||
var string = ""
|
var string = ""
|
||||||
|
if let indentation = indentation {
|
||||||
for token in tokens {
|
for token in tokens {
|
||||||
switch token {
|
if string.last == "\n" {
|
||||||
case let .text(text):
|
string += indentation
|
||||||
string += text
|
}
|
||||||
case let .variable(variable, method):
|
string += renderToken(token, stack: stack, context: context)
|
||||||
if let child = getChild(named: variable, from: object, method: method, context: context) {
|
}
|
||||||
if let template = child as? HBMustacheTemplate {
|
|
||||||
string += template.render(object)
|
|
||||||
} else {
|
} else {
|
||||||
string += String(describing: child).htmlEscape()
|
for token in tokens {
|
||||||
}
|
string += renderToken(token, stack: stack, context: context)
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return string
|
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
|
/// Render a section
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - child: Object to render section for
|
/// - child: Object to render section for
|
||||||
/// - parent: Current object being rendered
|
/// - parent: Current object being rendered
|
||||||
/// - template: Template to render with
|
/// - template: Template to render with
|
||||||
/// - Returns: Rendered text
|
/// - 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 {
|
switch child {
|
||||||
case let array as HBMustacheSequence:
|
case let array as HBMustacheSequence:
|
||||||
return array.renderSection(with: template)
|
return array.renderSection(with: template, stack: stack + [array])
|
||||||
case let bool as Bool:
|
case let bool as Bool:
|
||||||
return bool ? template.render(parent) : ""
|
return bool ? template.render(stack) : ""
|
||||||
case let lambda as HBMustacheLambda:
|
case let lambda as HBMustacheLambda:
|
||||||
return lambda.run(parent, template)
|
return lambda.run(stack.last!, template)
|
||||||
case let .some(value):
|
case let .some(value):
|
||||||
return template.render(value)
|
return template.render(stack + [value])
|
||||||
case .none:
|
case .none:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -67,33 +82,46 @@ extension HBMustacheTemplate {
|
|||||||
/// - parent: Current object being rendered
|
/// - parent: Current object being rendered
|
||||||
/// - template: Template to render with
|
/// - template: Template to render with
|
||||||
/// - Returns: Rendered text
|
/// - 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 {
|
switch child {
|
||||||
case let array as HBMustacheSequence:
|
case let array as HBMustacheSequence:
|
||||||
return array.renderInvertedSection(with: template)
|
return array.renderInvertedSection(with: template, stack: stack)
|
||||||
case let bool as Bool:
|
case let bool as Bool:
|
||||||
return bool ? "" : template.render(parent)
|
return bool ? "" : template.render(stack)
|
||||||
case .some:
|
case .some:
|
||||||
return ""
|
return ""
|
||||||
case .none:
|
case .none:
|
||||||
return template.render(parent)
|
return template.render(stack)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get child object from variable name
|
/// Get child object from variable name
|
||||||
func getChild(named name: String, from object: Any, method: String?, context: HBMustacheContext?) -> Any? {
|
func getChild(named name: String, from stack: [Any], method: String?, context: HBMustacheContext?) -> Any? {
|
||||||
func _getChild(named names: ArraySlice<String>, from object: Any) -> Any? {
|
func _getImmediateChild(named name: String, from object: Any) -> Any? {
|
||||||
guard let name = names.first else { return object }
|
|
||||||
let childObject: Any?
|
|
||||||
if let customBox = object as? HBMustacheParent {
|
if let customBox = object as? HBMustacheParent {
|
||||||
childObject = customBox.child(named: name)
|
return customBox.child(named: name)
|
||||||
} else {
|
} else {
|
||||||
let mirror = Mirror(reflecting: object)
|
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()
|
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 ""
|
// 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
|
// the name is split by "." and we use mirror to get the correct child object
|
||||||
let child: Any?
|
let child: Any?
|
||||||
if name == "." {
|
if name == "." {
|
||||||
child = object
|
child = stack.last!
|
||||||
} else if name == "", method != nil {
|
} else if name == "", method != nil {
|
||||||
child = context
|
child = context
|
||||||
} else {
|
} else {
|
||||||
let nameSplit = name.split(separator: ".").map { String($0) }
|
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
|
// if we want to run a method and the current child can have methods applied to it then
|
||||||
// run method on the current child
|
// run method on the current child
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public final class HBMustacheTemplate {
|
|||||||
/// - Parameter object: Object to render
|
/// - Parameter object: Object to render
|
||||||
/// - Returns: Rendered text
|
/// - Returns: Rendered text
|
||||||
public func render(_ object: Any) -> String {
|
public func render(_ object: Any) -> String {
|
||||||
render(object, context: nil)
|
render([object], context: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal init(_ tokens: [Token]) {
|
internal init(_ tokens: [Token]) {
|
||||||
@@ -36,7 +36,7 @@ public final class HBMustacheTemplate {
|
|||||||
case unescapedVariable(name: String, method: String? = nil)
|
case unescapedVariable(name: String, method: String? = nil)
|
||||||
case section(name: String, method: String? = nil, template: HBMustacheTemplate)
|
case section(name: String, method: String? = nil, template: HBMustacheTemplate)
|
||||||
case invertedSection(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]
|
let tokens: [Token]
|
||||||
|
|||||||
@@ -18,11 +18,28 @@ final class MethodTests: XCTestCase {
|
|||||||
XCTAssertEqual(template.render(object), "TEST")
|
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 {
|
func testFirstLast() throws {
|
||||||
let template = try HBMustacheTemplate(string: """
|
let template = try HBMustacheTemplate(string: """
|
||||||
{{#repo}}
|
{{#repo}}
|
||||||
<b>{{#first()}}first: {{/}}{{#last()}}last: {{/}}{{ name }}</b>
|
<b>{{#first()}}first: {{/}}{{#last()}}last: {{/}}{{ name }}</b>
|
||||||
{{/repo}}
|
{{/repo}}
|
||||||
|
|
||||||
""")
|
""")
|
||||||
let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]]
|
let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]]
|
||||||
XCTAssertEqual(template.render(object), """
|
XCTAssertEqual(template.render(object), """
|
||||||
@@ -38,6 +55,7 @@ final class MethodTests: XCTestCase {
|
|||||||
{{#repo}}
|
{{#repo}}
|
||||||
<b>{{#index()}}{{plusone(.)}}{{/}}) {{ name }}</b>
|
<b>{{#index()}}{{plusone(.)}}{{/}}) {{ name }}</b>
|
||||||
{{/repo}}
|
{{/repo}}
|
||||||
|
|
||||||
""")
|
""")
|
||||||
let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]]
|
let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]]
|
||||||
XCTAssertEqual(template.render(object), """
|
XCTAssertEqual(template.render(object), """
|
||||||
@@ -53,6 +71,7 @@ final class MethodTests: XCTestCase {
|
|||||||
{{#repo}}
|
{{#repo}}
|
||||||
<b>{{index()}}) {{#even()}}even {{/}}{{#odd()}}odd {{/}}{{ name }}</b>
|
<b>{{index()}}) {{#even()}}even {{/}}{{#odd()}}odd {{/}}{{ name }}</b>
|
||||||
{{/repo}}
|
{{/repo}}
|
||||||
|
|
||||||
""")
|
""")
|
||||||
let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]]
|
let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]]
|
||||||
XCTAssertEqual(template.render(object), """
|
XCTAssertEqual(template.render(object), """
|
||||||
@@ -68,6 +87,7 @@ final class MethodTests: XCTestCase {
|
|||||||
{{#reversed(repo)}}
|
{{#reversed(repo)}}
|
||||||
<b>{{ name }}</b>
|
<b>{{ name }}</b>
|
||||||
{{/repo}}
|
{{/repo}}
|
||||||
|
|
||||||
""")
|
""")
|
||||||
let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]]
|
let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]]
|
||||||
XCTAssertEqual(template.render(object), """
|
XCTAssertEqual(template.render(object), """
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ final class PartialTests: XCTestCase {
|
|||||||
""")
|
""")
|
||||||
let template2 = try HBMustacheTemplate(string: """
|
let template2 = try HBMustacheTemplate(string: """
|
||||||
<strong>{{.}}</strong>
|
<strong>{{.}}</strong>
|
||||||
|
|
||||||
""")
|
""")
|
||||||
library.register(template, named: "base")
|
library.register(template, named: "base")
|
||||||
library.register(template2, named: "user")
|
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
|
return lhs1 == rhs1 && lhs2 == rhs2 && lhs3 == rhs3
|
||||||
case let (.invertedSection(lhs1, lhs2, lhs3), .invertedSection(rhs1, rhs2, rhs3)):
|
case let (.invertedSection(lhs1, lhs2, lhs3), .invertedSection(rhs1, rhs2, rhs3)):
|
||||||
return lhs1 == rhs1 && lhs2 == rhs2 && lhs3 == rhs3
|
return lhs1 == rhs1 && lhs2 == rhs2 && lhs3 == rhs3
|
||||||
case let (.partial(name1), .partial(name2)):
|
case let (.partial(name1, indent1), .partial(name2, indent2)):
|
||||||
return name1 == name2
|
return name1 == name2 && indent1 == indent2
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user