Add support for proper lambdas (#48)
* Add support for proper lambdas * Get rid of recursion Remove renderSectionLambda as I can use renderUnescapedLambda for that.
This commit is contained in:
@@ -34,7 +34,7 @@
|
|||||||
///
|
///
|
||||||
public struct MustacheLambda {
|
public struct MustacheLambda {
|
||||||
/// lambda callback
|
/// lambda callback
|
||||||
public typealias Callback = (Any, MustacheTemplate) -> String
|
public typealias Callback = (String) -> Any?
|
||||||
|
|
||||||
let callback: Callback
|
let callback: Callback
|
||||||
|
|
||||||
@@ -44,7 +44,13 @@ public struct MustacheLambda {
|
|||||||
self.callback = cb
|
self.callback = cb
|
||||||
}
|
}
|
||||||
|
|
||||||
internal func run(_ object: Any, _ template: MustacheTemplate) -> String {
|
/// Initialize `MustacheLambda`
|
||||||
return self.callback(object, template)
|
/// - Parameter cb: function to be called by lambda
|
||||||
|
public init(_ cb: @escaping () -> Any?) {
|
||||||
|
self.callback = { _ in cb() }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal func callAsFunction(_ s: String) -> Any? {
|
||||||
|
return self.callback(s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,15 @@ extension Parser {
|
|||||||
return subString
|
return subString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read until we hit string index
|
||||||
|
/// - Parameter until: Read until position
|
||||||
|
/// - Returns: The string read from the buffer
|
||||||
|
mutating func read(until: String.Index) -> Substring {
|
||||||
|
let string = self.buffer[self.position..<until]
|
||||||
|
self.position = until
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
|
||||||
/// Read from buffer until we hit a character. Position after this is of the character we were checking for
|
/// Read from buffer until we hit a character. Position after this is of the character we were checking for
|
||||||
/// - Parameter until: Character to read until
|
/// - Parameter until: Character to read until
|
||||||
/// - Throws: .overflow if we hit the end of the buffer before reading character
|
/// - Throws: .overflow if we hit the end of the buffer before reading character
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ extension MustacheTemplate {
|
|||||||
let fs = FileManager()
|
let fs = FileManager()
|
||||||
guard let data = fs.contents(atPath: filename) else { return nil }
|
guard let data = fs.contents(atPath: filename) else { return nil }
|
||||||
let string = String(decoding: data, as: Unicode.UTF8.self)
|
let string = String(decoding: data, as: Unicode.UTF8.self)
|
||||||
self.tokens = try Self.parse(string)
|
let template = try Self.parse(string)
|
||||||
|
self.tokens = template.tokens
|
||||||
|
self.text = string
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ extension MustacheTemplate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// parse mustache text to generate a list of tokens
|
/// parse mustache text to generate a list of tokens
|
||||||
static func parse(_ string: String) throws -> [Token] {
|
static func parse(_ string: String) throws -> MustacheTemplate {
|
||||||
var parser = Parser(string)
|
var parser = Parser(string)
|
||||||
do {
|
do {
|
||||||
return try self.parse(&parser, state: .init())
|
return try self.parse(&parser, state: .init())
|
||||||
@@ -117,10 +117,11 @@ extension MustacheTemplate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// parse section in mustache text
|
/// parse section in mustache text
|
||||||
static func parse(_ parser: inout Parser, state: ParserState) throws -> [Token] {
|
static func parse(_ parser: inout Parser, state: ParserState) throws -> MustacheTemplate {
|
||||||
var tokens: [Token] = []
|
var tokens: [Token] = []
|
||||||
var state = state
|
var state = state
|
||||||
var whiteSpaceBefore: Substring = ""
|
var whiteSpaceBefore: Substring = ""
|
||||||
|
var origParser = parser
|
||||||
while !parser.reachedEnd() {
|
while !parser.reachedEnd() {
|
||||||
// if new line read whitespace
|
// if new line read whitespace
|
||||||
if state.newLine {
|
if state.newLine {
|
||||||
@@ -169,8 +170,8 @@ extension MustacheTemplate {
|
|||||||
tokens.append(.text(String(whiteSpaceBefore)))
|
tokens.append(.text(String(whiteSpaceBefore)))
|
||||||
whiteSpaceBefore = ""
|
whiteSpaceBefore = ""
|
||||||
}
|
}
|
||||||
let sectionTokens = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine, transforms: transforms))
|
let sectionTemplate = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine, transforms: transforms))
|
||||||
tokens.append(.section(name: name, transforms: transforms, template: MustacheTemplate(sectionTokens)))
|
tokens.append(.section(name: name, transforms: transforms, template: sectionTemplate))
|
||||||
|
|
||||||
case "^":
|
case "^":
|
||||||
// inverted section
|
// inverted section
|
||||||
@@ -182,11 +183,17 @@ extension MustacheTemplate {
|
|||||||
tokens.append(.text(String(whiteSpaceBefore)))
|
tokens.append(.text(String(whiteSpaceBefore)))
|
||||||
whiteSpaceBefore = ""
|
whiteSpaceBefore = ""
|
||||||
}
|
}
|
||||||
let sectionTokens = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine, transforms: transforms))
|
let sectionTemplate = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine, transforms: transforms))
|
||||||
tokens.append(.invertedSection(name: name, transforms: transforms, template: MustacheTemplate(sectionTokens)))
|
tokens.append(.invertedSection(name: name, transforms: transforms, template: sectionTemplate))
|
||||||
|
|
||||||
case "/":
|
case "/":
|
||||||
// end of section
|
// end of section
|
||||||
|
|
||||||
|
// record end of section text
|
||||||
|
var sectionParser = parser
|
||||||
|
sectionParser.unsafeRetreat()
|
||||||
|
sectionParser.unsafeRetreat()
|
||||||
|
|
||||||
parser.unsafeAdvance()
|
parser.unsafeAdvance()
|
||||||
let position = parser.position
|
let position = parser.position
|
||||||
let (name, transforms) = try parseName(&parser, state: state)
|
let (name, transforms) = try parseName(&parser, state: state)
|
||||||
@@ -200,7 +207,7 @@ extension MustacheTemplate {
|
|||||||
tokens.append(.text(String(whiteSpaceBefore)))
|
tokens.append(.text(String(whiteSpaceBefore)))
|
||||||
whiteSpaceBefore = ""
|
whiteSpaceBefore = ""
|
||||||
}
|
}
|
||||||
return tokens
|
return .init(tokens, text: String(origParser.read(until: sectionParser.position)))
|
||||||
|
|
||||||
case "!":
|
case "!":
|
||||||
// comment
|
// comment
|
||||||
@@ -280,10 +287,10 @@ extension MustacheTemplate {
|
|||||||
if self.isStandalone(&parser, state: state) {
|
if self.isStandalone(&parser, state: state) {
|
||||||
setNewLine = true
|
setNewLine = true
|
||||||
}
|
}
|
||||||
let sectionTokens = try parse(&parser, state: state.withInheritancePartial(sectionName))
|
let sectionTemplate = try parse(&parser, state: state.withInheritancePartial(sectionName))
|
||||||
var inherit: [String: MustacheTemplate] = [:]
|
var inherit: [String: MustacheTemplate] = [:]
|
||||||
// parse tokens in section to extract inherited sections
|
// parse tokens in section to extract inherited sections
|
||||||
for token in sectionTokens {
|
for token in sectionTemplate.tokens {
|
||||||
switch token {
|
switch token {
|
||||||
case .blockDefinition(let name, let template):
|
case .blockDefinition(let name, let template):
|
||||||
inherit[name] = template
|
inherit[name] = template
|
||||||
@@ -311,8 +318,8 @@ extension MustacheTemplate {
|
|||||||
if standAlone {
|
if standAlone {
|
||||||
setNewLine = true
|
setNewLine = true
|
||||||
}
|
}
|
||||||
let sectionTokens = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine))
|
let sectionTemplate = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine))
|
||||||
tokens.append(.blockDefinition(name: name, template: MustacheTemplate(sectionTokens)))
|
tokens.append(.blockDefinition(name: name, template: sectionTemplate))
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if whiteSpaceBefore.count > 0 {
|
if whiteSpaceBefore.count > 0 {
|
||||||
@@ -321,8 +328,8 @@ extension MustacheTemplate {
|
|||||||
if self.isStandalone(&parser, state: state) {
|
if self.isStandalone(&parser, state: state) {
|
||||||
setNewLine = true
|
setNewLine = true
|
||||||
} else if whiteSpaceBefore.count > 0 {}
|
} else if whiteSpaceBefore.count > 0 {}
|
||||||
let sectionTokens = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine))
|
let sectionTemplate = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine))
|
||||||
tokens.append(.blockExpansion(name: name, default: MustacheTemplate(sectionTokens), indentation: String(whiteSpaceBefore)))
|
tokens.append(.blockExpansion(name: name, default: sectionTemplate, indentation: String(whiteSpaceBefore)))
|
||||||
whiteSpaceBefore = ""
|
whiteSpaceBefore = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,7 +362,7 @@ extension MustacheTemplate {
|
|||||||
guard state.sectionName == nil else {
|
guard state.sectionName == nil else {
|
||||||
throw Error.expectedSectionEnd
|
throw Error.expectedSectionEnd
|
||||||
}
|
}
|
||||||
return tokens
|
return .init(tokens, text: String(origParser.read(until: parser.position)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// read until we hit either the start delimiter of a tag or a newline
|
/// read until we hit either the start delimiter of a tag or a newline
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ extension MustacheTemplate {
|
|||||||
return template.render(context: context)
|
return template.render(context: context)
|
||||||
} else if let renderable = child as? MustacheCustomRenderable {
|
} else if let renderable = child as? MustacheCustomRenderable {
|
||||||
return context.contentType.escapeText(renderable.renderText)
|
return context.contentType.escapeText(renderable.renderText)
|
||||||
|
} else if let lambda = child as? MustacheLambda {
|
||||||
|
return self.renderLambda(lambda, parameter: "", context: context)
|
||||||
} else {
|
} else {
|
||||||
return context.contentType.escapeText(String(describing: child))
|
return context.contentType.escapeText(String(describing: child))
|
||||||
}
|
}
|
||||||
@@ -63,6 +65,8 @@ extension MustacheTemplate {
|
|||||||
if let child = getChild(named: variable, transforms: transforms, context: context) {
|
if let child = getChild(named: variable, transforms: transforms, context: context) {
|
||||||
if let renderable = child as? MustacheCustomRenderable {
|
if let renderable = child as? MustacheCustomRenderable {
|
||||||
return renderable.renderText
|
return renderable.renderText
|
||||||
|
} else if let lambda = child as? MustacheLambda {
|
||||||
|
return self.renderUnescapedLambda(lambda, parameter: "", context: context)
|
||||||
} else {
|
} else {
|
||||||
return String(describing: child)
|
return String(describing: child)
|
||||||
}
|
}
|
||||||
@@ -70,6 +74,9 @@ extension MustacheTemplate {
|
|||||||
|
|
||||||
case .section(let variable, let transforms, let template):
|
case .section(let variable, let transforms, let template):
|
||||||
let child = self.getChild(named: variable, transforms: transforms, context: context)
|
let child = self.getChild(named: variable, transforms: transforms, context: context)
|
||||||
|
if let lambda = child as? MustacheLambda {
|
||||||
|
return self.renderUnescapedLambda(lambda, parameter: template.text, context: context)
|
||||||
|
}
|
||||||
return self.renderSection(child, with: template, context: context)
|
return self.renderSection(child, with: template, context: context)
|
||||||
|
|
||||||
case .invertedSection(let variable, let transforms, let template):
|
case .invertedSection(let variable, let transforms, let template):
|
||||||
@@ -144,8 +151,6 @@ extension MustacheTemplate {
|
|||||||
return array.renderSection(with: template, context: context)
|
return array.renderSection(with: template, context: context)
|
||||||
case let bool as Bool:
|
case let bool as Bool:
|
||||||
return bool ? template.render(context: context) : ""
|
return bool ? template.render(context: context) : ""
|
||||||
case let lambda as MustacheLambda:
|
|
||||||
return lambda.run(context.stack.last!, template)
|
|
||||||
case let null as MustacheCustomRenderable where null.isNull == true:
|
case let null as MustacheCustomRenderable where null.isNull == true:
|
||||||
return ""
|
return ""
|
||||||
case .some(let value):
|
case .some(let value):
|
||||||
@@ -176,19 +181,67 @@ extension MustacheTemplate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func renderLambda(_ lambda: MustacheLambda, parameter: String, context: MustacheContext) -> String {
|
||||||
|
var lambda = lambda
|
||||||
|
while true {
|
||||||
|
guard let result = lambda(parameter) else { return "" }
|
||||||
|
if let string = result as? String {
|
||||||
|
do {
|
||||||
|
let newTemplate = try MustacheTemplate(string: context.contentType.escapeText(string))
|
||||||
|
return self.renderSection(context.stack.last, with: newTemplate, context: context)
|
||||||
|
} catch {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
} else if let lambda2 = result as? MustacheLambda {
|
||||||
|
lambda = lambda2
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
return context.contentType.escapeText(String(describing: result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderUnescapedLambda(_ lambda: MustacheLambda, parameter: String, context: MustacheContext) -> String {
|
||||||
|
var lambda = lambda
|
||||||
|
while true {
|
||||||
|
guard let result = lambda(parameter) else { return "" }
|
||||||
|
if let string = result as? String {
|
||||||
|
do {
|
||||||
|
let newTemplate = try MustacheTemplate(string: string)
|
||||||
|
return self.renderSection(context.stack.last, with: newTemplate, context: context)
|
||||||
|
} catch {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
} else if let lambda2 = result as? MustacheLambda {
|
||||||
|
lambda = lambda2
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
return String(describing: result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get child object from variable name
|
/// Get child object from variable name
|
||||||
func getChild(named name: String, transforms: [String], context: MustacheContext) -> Any? {
|
func getChild(named name: String, transforms: [String], context: MustacheContext) -> Any? {
|
||||||
func _getImmediateChild(named name: String, from object: Any) -> Any? {
|
func _getImmediateChild(named name: String, from object: Any) -> Any? {
|
||||||
|
let object = {
|
||||||
if let customBox = object as? MustacheParent {
|
if let customBox = object as? MustacheParent {
|
||||||
return customBox.child(named: name)
|
return customBox.child(named: name)
|
||||||
} else {
|
} else {
|
||||||
let mirror = Mirror(reflecting: object)
|
let mirror = Mirror(reflecting: object)
|
||||||
return mirror.getValue(forKey: name)
|
return mirror.getValue(forKey: name)
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
return object
|
||||||
}
|
}
|
||||||
|
|
||||||
func _getChild(named names: ArraySlice<String>, from object: Any) -> Any? {
|
func _getChild(named names: ArraySlice<String>, from object: Any) -> Any? {
|
||||||
guard let name = names.first else { return object }
|
guard let name = names.first else { return object }
|
||||||
|
var object = object
|
||||||
|
if let lambda = object as? MustacheLambda {
|
||||||
|
guard let result = lambda("") else { return nil }
|
||||||
|
object = result
|
||||||
|
}
|
||||||
guard let childObject = _getImmediateChild(named: name, from: object) else { return nil }
|
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)
|
||||||
|
|||||||
@@ -13,12 +13,14 @@
|
|||||||
//===----------------------------------------------------------------------===//
|
//===----------------------------------------------------------------------===//
|
||||||
|
|
||||||
/// Class holding Mustache template
|
/// Class holding Mustache template
|
||||||
public struct MustacheTemplate: Sendable {
|
public struct MustacheTemplate: Sendable, CustomStringConvertible {
|
||||||
/// Initialize template
|
/// Initialize template
|
||||||
/// - Parameter string: Template text
|
/// - Parameter string: Template text
|
||||||
/// - Throws: MustacheTemplate.Error
|
/// - Throws: MustacheTemplate.Error
|
||||||
public init(string: String) throws {
|
public init(string: String) throws {
|
||||||
self.tokens = try Self.parse(string)
|
let template = try Self.parse(string)
|
||||||
|
self.tokens = template.tokens
|
||||||
|
self.text = string
|
||||||
self.filename = nil
|
self.filename = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,12 +56,15 @@ public struct MustacheTemplate: Sendable {
|
|||||||
return self.render(context: .init(object, library: library))
|
return self.render(context: .init(object, library: library))
|
||||||
}
|
}
|
||||||
|
|
||||||
internal init(_ tokens: [Token]) {
|
internal init(_ tokens: [Token], text: String) {
|
||||||
self.tokens = tokens
|
self.tokens = tokens
|
||||||
self.filename = nil
|
self.filename = nil
|
||||||
|
self.text = text
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Token: Sendable {
|
public var description: String { self.text }
|
||||||
|
|
||||||
|
enum Token: Sendable /* , CustomStringConvertible */ {
|
||||||
case text(String)
|
case text(String)
|
||||||
case variable(name: String, transforms: [String] = [])
|
case variable(name: String, transforms: [String] = [])
|
||||||
case unescapedVariable(name: String, transforms: [String] = [])
|
case unescapedVariable(name: String, transforms: [String] = [])
|
||||||
@@ -73,5 +78,6 @@ public struct MustacheTemplate: Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var tokens: [Token]
|
var tokens: [Token]
|
||||||
|
let text: String
|
||||||
let filename: String?
|
let filename: String?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ final class MustacheSpecTests: XCTestCase {
|
|||||||
struct Test: Decodable {
|
struct Test: Decodable {
|
||||||
let name: String
|
let name: String
|
||||||
let desc: String
|
let desc: String
|
||||||
let data: AnyDecodable
|
var data: AnyDecodable
|
||||||
let partials: [String: String]?
|
let partials: [String: String]?
|
||||||
let template: String
|
let template: String
|
||||||
let expected: String
|
let expected: String
|
||||||
@@ -155,6 +155,49 @@ final class MustacheSpecTests: XCTestCase {
|
|||||||
print(-date.timeIntervalSinceNow)
|
print(-date.timeIntervalSinceNow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testLambdaSpec() async throws {
|
||||||
|
var g = 0
|
||||||
|
let lambdaMap = [
|
||||||
|
"Interpolation": MustacheLambda { "world" },
|
||||||
|
"Interpolation - Expansion": MustacheLambda { "{{planet}}" },
|
||||||
|
"Interpolation - Alternate Delimiters": MustacheLambda { "|planet| => {{planet}}" },
|
||||||
|
"Interpolation - Multiple Calls": MustacheLambda { return MustacheLambda { g += 1; return g }},
|
||||||
|
"Escaping": MustacheLambda { ">" },
|
||||||
|
"Section": MustacheLambda { text in text == "{{x}}" ? "yes" : "no" },
|
||||||
|
"Section - Expansion": MustacheLambda { text in text + "{{planet}}" + text },
|
||||||
|
// Not going to bother implementing this requires pushing alternate delimiters through the context
|
||||||
|
// "Section - Alternate Delimiters": MustacheLambda { text in return text + "{{planet}} => |planet|" + text },
|
||||||
|
"Section - Multiple Calls": MustacheLambda { text in "__" + text + "__" },
|
||||||
|
"Inverted Section": MustacheLambda { false },
|
||||||
|
]
|
||||||
|
let url = URL(string: "https://raw.githubusercontent.com/mustache/spec/master/specs/~lambdas.json")!
|
||||||
|
#if compiler(>=6.0)
|
||||||
|
let (data, _) = try await URLSession.shared.data(from: url)
|
||||||
|
#else
|
||||||
|
let data = try Data(contentsOf: url)
|
||||||
|
#endif
|
||||||
|
let spec = try JSONDecoder().decode(Spec.self, from: data)
|
||||||
|
// edit spec and replace lambda with Swift lambda
|
||||||
|
let editedSpecTests = spec.tests.compactMap { test -> Spec.Test? in
|
||||||
|
var test = test
|
||||||
|
var newTestData: [String: Any] = [:]
|
||||||
|
guard let dictionary = test.data.value as? [String: Any] else { return nil }
|
||||||
|
for values in dictionary {
|
||||||
|
newTestData[values.key] = values.value
|
||||||
|
}
|
||||||
|
guard let lambda = lambdaMap[test.name] else { return nil }
|
||||||
|
newTestData["lambda"] = lambda
|
||||||
|
test.data = .init(newTestData)
|
||||||
|
return test
|
||||||
|
}
|
||||||
|
|
||||||
|
let date = Date()
|
||||||
|
for test in editedSpecTests {
|
||||||
|
XCTAssertNoThrow(try test.run())
|
||||||
|
}
|
||||||
|
print(-date.timeIntervalSinceNow)
|
||||||
|
}
|
||||||
|
|
||||||
func testCommentsSpec() async throws {
|
func testCommentsSpec() async throws {
|
||||||
try await self.testSpec(name: "comments")
|
try await self.testSpec(name: "comments")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ final class TemplateParserTests: XCTestCase {
|
|||||||
|
|
||||||
func testSection() throws {
|
func testSection() throws {
|
||||||
let template = try MustacheTemplate(string: "test {{#section}}text{{/section}}")
|
let template = try MustacheTemplate(string: "test {{#section}}text{{/section}}")
|
||||||
XCTAssertEqual(template.tokens, [.text("test "), .section(name: "section", template: .init([.text("text")]))])
|
XCTAssertEqual(template.tokens, [.text("test "), .section(name: "section", template: .init([.text("text")], text: "text"))])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testInvertedSection() throws {
|
func testInvertedSection() throws {
|
||||||
let template = try MustacheTemplate(string: "test {{^section}}text{{/section}}")
|
let template = try MustacheTemplate(string: "test {{^section}}text{{/section}}")
|
||||||
XCTAssertEqual(template.tokens, [.text("test "), .invertedSection(name: "section", template: .init([.text("text")]))])
|
XCTAssertEqual(template.tokens, [.text("test "), .invertedSection(name: "section", template: .init([.text("text")], text: "text"))])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testComment() throws {
|
func testComment() throws {
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ final class TemplateRendererTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// variables
|
/// variables
|
||||||
func testMustacheManualExample1() throws {
|
func testMustacheManualVariables() throws {
|
||||||
let template = try MustacheTemplate(string: """
|
let template = try MustacheTemplate(string: """
|
||||||
Hello {{name}}
|
Hello {{name}}
|
||||||
You have just won {{value}} dollars!
|
You have just won {{value}} dollars!
|
||||||
@@ -157,8 +157,8 @@ final class TemplateRendererTests: XCTestCase {
|
|||||||
""")
|
""")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// test esacped and unescaped text
|
/// test escaped and unescaped text
|
||||||
func testMustacheManualExample2() throws {
|
func testMustacheManualEscapedText() throws {
|
||||||
let template = try MustacheTemplate(string: """
|
let template = try MustacheTemplate(string: """
|
||||||
*{{name}}
|
*{{name}}
|
||||||
*{{age}}
|
*{{age}}
|
||||||
@@ -174,8 +174,71 @@ final class TemplateRendererTests: XCTestCase {
|
|||||||
""")
|
""")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// test dotted names
|
||||||
|
func test_MustacheManualDottedNames() throws {
|
||||||
|
let template = try MustacheTemplate(string: """
|
||||||
|
* {{client.name}}
|
||||||
|
* {{age}}
|
||||||
|
* {{client.company.name}}
|
||||||
|
* {{{company.name}}}
|
||||||
|
""")
|
||||||
|
let object: [String: Any] = [
|
||||||
|
"client": (
|
||||||
|
name: "Chris & Friends",
|
||||||
|
age: 50
|
||||||
|
),
|
||||||
|
"company": [
|
||||||
|
"name": "<b>GitHub</b>",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
XCTAssertEqual(template.render(object), """
|
||||||
|
* Chris & Friends
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* <b>GitHub</b>
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// test implicit operator
|
||||||
|
func testMustacheManualImplicitOperator() throws {
|
||||||
|
let template = try MustacheTemplate(string: """
|
||||||
|
* {{.}}
|
||||||
|
""")
|
||||||
|
let object = "Hello!"
|
||||||
|
XCTAssertEqual(template.render(object), """
|
||||||
|
* Hello!
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// test lambda
|
||||||
|
func test_MustacheManualLambda() throws {
|
||||||
|
let template = try MustacheTemplate(string: """
|
||||||
|
* {{time.hour}}
|
||||||
|
* {{today}}
|
||||||
|
""")
|
||||||
|
let object: [String: Any] = [
|
||||||
|
"year": 1970,
|
||||||
|
"month": 1,
|
||||||
|
"day": 1,
|
||||||
|
"time": MustacheLambda { _ in
|
||||||
|
return (
|
||||||
|
hour: 0,
|
||||||
|
minute: 0,
|
||||||
|
second: 0
|
||||||
|
)
|
||||||
|
},
|
||||||
|
"today": MustacheLambda { _ in
|
||||||
|
return "{{year}}-{{month}}-{{day}}"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
XCTAssertEqual(template.render(object), """
|
||||||
|
* 0
|
||||||
|
* 1970-1-1
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
/// test boolean
|
/// test boolean
|
||||||
func testMustacheManualExample3() throws {
|
func testMustacheManualSectionFalse() throws {
|
||||||
let template = try MustacheTemplate(string: """
|
let template = try MustacheTemplate(string: """
|
||||||
Shown.
|
Shown.
|
||||||
{{#person}}
|
{{#person}}
|
||||||
@@ -190,7 +253,7 @@ final class TemplateRendererTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// test non-empty lists
|
/// test non-empty lists
|
||||||
func testMustacheManualExample4() throws {
|
func testMustacheManualSectionList() throws {
|
||||||
let template = try MustacheTemplate(string: """
|
let template = try MustacheTemplate(string: """
|
||||||
{{#repo}}
|
{{#repo}}
|
||||||
<b>{{name}}</b>
|
<b>{{name}}</b>
|
||||||
@@ -205,13 +268,29 @@ final class TemplateRendererTests: XCTestCase {
|
|||||||
""")
|
""")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// test non-empty lists
|
||||||
|
func testMustacheManualSectionList2() throws {
|
||||||
|
let template = try MustacheTemplate(string: """
|
||||||
|
{{#repo}}
|
||||||
|
<b>{{.}}</b>
|
||||||
|
{{/repo}}
|
||||||
|
""")
|
||||||
|
let object: [String: Any] = ["repo": ["resque", "hub", "rip"]]
|
||||||
|
XCTAssertEqual(template.render(object), """
|
||||||
|
<b>resque</b>
|
||||||
|
<b>hub</b>
|
||||||
|
<b>rip</b>
|
||||||
|
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
/// test lambdas
|
/// test lambdas
|
||||||
func testMustacheManualExample5() throws {
|
func testMustacheManualSectionLambda() throws {
|
||||||
let template = try MustacheTemplate(string: """
|
let template = try MustacheTemplate(string: """
|
||||||
{{#wrapped}}{{name}} is awesome.{{/wrapped}}
|
{{#wrapped}}{{name}} is awesome.{{/wrapped}}
|
||||||
""")
|
""")
|
||||||
func wrapped(object: Any, template: MustacheTemplate) -> String {
|
func wrapped(_ s: String) -> Any? {
|
||||||
return "<b>\(template.render(object))</b>"
|
return "<b>\(s)</b>"
|
||||||
}
|
}
|
||||||
let object: [String: Any] = ["name": "Willy", "wrapped": MustacheLambda(wrapped)]
|
let object: [String: Any] = ["name": "Willy", "wrapped": MustacheLambda(wrapped)]
|
||||||
XCTAssertEqual(template.render(object), """
|
XCTAssertEqual(template.render(object), """
|
||||||
@@ -220,7 +299,7 @@ final class TemplateRendererTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// test setting context object
|
/// test setting context object
|
||||||
func testMustacheManualExample6() throws {
|
func testMustacheManualContextObject() throws {
|
||||||
let template = try MustacheTemplate(string: """
|
let template = try MustacheTemplate(string: """
|
||||||
{{#person?}}
|
{{#person?}}
|
||||||
Hi {{name}}!
|
Hi {{name}}!
|
||||||
@@ -234,7 +313,7 @@ final class TemplateRendererTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// test inverted sections
|
/// test inverted sections
|
||||||
func testMustacheManualExample7() throws {
|
func testMustacheManualInvertedSection() throws {
|
||||||
let template = try MustacheTemplate(string: """
|
let template = try MustacheTemplate(string: """
|
||||||
{{#repo}}
|
{{#repo}}
|
||||||
<b>{{name}}</b>
|
<b>{{name}}</b>
|
||||||
@@ -251,7 +330,7 @@ final class TemplateRendererTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// test comments
|
/// test comments
|
||||||
func testMustacheManualExample8() throws {
|
func testMustacheManualComment() throws {
|
||||||
let template = try MustacheTemplate(string: """
|
let template = try MustacheTemplate(string: """
|
||||||
<h1>Today{{! ignore me }}.</h1>
|
<h1>Today{{! ignore me }}.</h1>
|
||||||
""")
|
""")
|
||||||
|
|||||||
Reference in New Issue
Block a user