diff --git a/Sources/HummingbirdMustache/Sequence.swift b/Sources/HummingbirdMustache/Sequence.swift index 6105d7d..498fd8b 100644 --- a/Sources/HummingbirdMustache/Sequence.swift +++ b/Sources/HummingbirdMustache/Sequence.swift @@ -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 "" } diff --git a/Sources/HummingbirdMustache/Template+Render.swift b/Sources/HummingbirdMustache/Template+Render.swift index 870182a..2bbe212 100644 --- a/Sources/HummingbirdMustache/Template+Render.swift +++ b/Sources/HummingbirdMustache/Template+Render.swift @@ -5,7 +5,7 @@ extension HBMustacheTemplate { /// - object: Object /// - context: Context that render is occurring in. Contains information about position in sequence /// - Returns: Rendered text - func render(_ object: Any, context: HBMustacheContext? = nil, indentation: String? = nil) -> String { + func render(_ object: [Any], context: HBMustacheContext? = nil, indentation: String? = nil) -> String { var string = "" for token in tokens { if let indentation = indentation, string.last == "\n" { @@ -28,11 +28,11 @@ extension HBMustacheTemplate { } case let .section(variable, method, template): let child = getChild(named: variable, from: object, method: method, context: context) - string += renderSection(child, parent: object, with: template) + string += renderSection(child, stack: 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) + string += renderInvertedSection(child, stack: object, with: template) case let .partial(name, indentation): if let template = library?.getTemplate(named: name) { @@ -49,16 +49,16 @@ extension HBMustacheTemplate { /// - 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 "" } @@ -70,33 +70,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, 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, 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, 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 "" @@ -104,12 +117,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 diff --git a/Sources/HummingbirdMustache/Template.swift b/Sources/HummingbirdMustache/Template.swift index 2a63ca0..c2d69e8 100644 --- a/Sources/HummingbirdMustache/Template.swift +++ b/Sources/HummingbirdMustache/Template.swift @@ -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]) { diff --git a/Tests/HummingbirdMustacheTests/SpecTests.swift b/Tests/HummingbirdMustacheTests/SpecTests.swift index 9343e68..584c32a 100644 --- a/Tests/HummingbirdMustacheTests/SpecTests.swift +++ b/Tests/HummingbirdMustacheTests/SpecTests.swift @@ -669,6 +669,258 @@ final class SpecSectionTests: XCTestCase { 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) } }