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 {
|
||||
/// lambda callback
|
||||
public typealias Callback = (Any, MustacheTemplate) -> String
|
||||
public typealias Callback = (String) -> Any?
|
||||
|
||||
let callback: Callback
|
||||
|
||||
@@ -44,7 +44,13 @@ public struct MustacheLambda {
|
||||
self.callback = cb
|
||||
}
|
||||
|
||||
internal func run(_ object: Any, _ template: MustacheTemplate) -> String {
|
||||
return self.callback(object, template)
|
||||
/// Initialize `MustacheLambda`
|
||||
/// - 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
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// - Parameter until: Character to read until
|
||||
/// - Throws: .overflow if we hit the end of the buffer before reading character
|
||||
|
||||
@@ -24,7 +24,9 @@ extension MustacheTemplate {
|
||||
let fs = FileManager()
|
||||
guard let data = fs.contents(atPath: filename) else { return nil }
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ extension MustacheTemplate {
|
||||
}
|
||||
|
||||
/// 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)
|
||||
do {
|
||||
return try self.parse(&parser, state: .init())
|
||||
@@ -117,10 +117,11 @@ extension MustacheTemplate {
|
||||
}
|
||||
|
||||
/// 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 state = state
|
||||
var whiteSpaceBefore: Substring = ""
|
||||
var origParser = parser
|
||||
while !parser.reachedEnd() {
|
||||
// if new line read whitespace
|
||||
if state.newLine {
|
||||
@@ -169,8 +170,8 @@ extension MustacheTemplate {
|
||||
tokens.append(.text(String(whiteSpaceBefore)))
|
||||
whiteSpaceBefore = ""
|
||||
}
|
||||
let sectionTokens = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine, transforms: transforms))
|
||||
tokens.append(.section(name: name, transforms: transforms, template: MustacheTemplate(sectionTokens)))
|
||||
let sectionTemplate = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine, transforms: transforms))
|
||||
tokens.append(.section(name: name, transforms: transforms, template: sectionTemplate))
|
||||
|
||||
case "^":
|
||||
// inverted section
|
||||
@@ -182,11 +183,17 @@ extension MustacheTemplate {
|
||||
tokens.append(.text(String(whiteSpaceBefore)))
|
||||
whiteSpaceBefore = ""
|
||||
}
|
||||
let sectionTokens = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine, transforms: transforms))
|
||||
tokens.append(.invertedSection(name: name, transforms: transforms, template: MustacheTemplate(sectionTokens)))
|
||||
let sectionTemplate = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine, transforms: transforms))
|
||||
tokens.append(.invertedSection(name: name, transforms: transforms, template: sectionTemplate))
|
||||
|
||||
case "/":
|
||||
// end of section
|
||||
|
||||
// record end of section text
|
||||
var sectionParser = parser
|
||||
sectionParser.unsafeRetreat()
|
||||
sectionParser.unsafeRetreat()
|
||||
|
||||
parser.unsafeAdvance()
|
||||
let position = parser.position
|
||||
let (name, transforms) = try parseName(&parser, state: state)
|
||||
@@ -200,7 +207,7 @@ extension MustacheTemplate {
|
||||
tokens.append(.text(String(whiteSpaceBefore)))
|
||||
whiteSpaceBefore = ""
|
||||
}
|
||||
return tokens
|
||||
return .init(tokens, text: String(origParser.read(until: sectionParser.position)))
|
||||
|
||||
case "!":
|
||||
// comment
|
||||
@@ -280,10 +287,10 @@ extension MustacheTemplate {
|
||||
if self.isStandalone(&parser, state: state) {
|
||||
setNewLine = true
|
||||
}
|
||||
let sectionTokens = try parse(&parser, state: state.withInheritancePartial(sectionName))
|
||||
let sectionTemplate = try parse(&parser, state: state.withInheritancePartial(sectionName))
|
||||
var inherit: [String: MustacheTemplate] = [:]
|
||||
// parse tokens in section to extract inherited sections
|
||||
for token in sectionTokens {
|
||||
for token in sectionTemplate.tokens {
|
||||
switch token {
|
||||
case .blockDefinition(let name, let template):
|
||||
inherit[name] = template
|
||||
@@ -311,8 +318,8 @@ extension MustacheTemplate {
|
||||
if standAlone {
|
||||
setNewLine = true
|
||||
}
|
||||
let sectionTokens = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine))
|
||||
tokens.append(.blockDefinition(name: name, template: MustacheTemplate(sectionTokens)))
|
||||
let sectionTemplate = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine))
|
||||
tokens.append(.blockDefinition(name: name, template: sectionTemplate))
|
||||
|
||||
} else {
|
||||
if whiteSpaceBefore.count > 0 {
|
||||
@@ -321,8 +328,8 @@ extension MustacheTemplate {
|
||||
if self.isStandalone(&parser, state: state) {
|
||||
setNewLine = true
|
||||
} else if whiteSpaceBefore.count > 0 {}
|
||||
let sectionTokens = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine))
|
||||
tokens.append(.blockExpansion(name: name, default: MustacheTemplate(sectionTokens), indentation: String(whiteSpaceBefore)))
|
||||
let sectionTemplate = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine))
|
||||
tokens.append(.blockExpansion(name: name, default: sectionTemplate, indentation: String(whiteSpaceBefore)))
|
||||
whiteSpaceBefore = ""
|
||||
}
|
||||
|
||||
@@ -355,7 +362,7 @@ extension MustacheTemplate {
|
||||
guard state.sectionName == nil else {
|
||||
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
|
||||
|
||||
@@ -54,6 +54,8 @@ extension MustacheTemplate {
|
||||
return template.render(context: context)
|
||||
} else if let renderable = child as? MustacheCustomRenderable {
|
||||
return context.contentType.escapeText(renderable.renderText)
|
||||
} else if let lambda = child as? MustacheLambda {
|
||||
return self.renderLambda(lambda, parameter: "", context: context)
|
||||
} else {
|
||||
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 renderable = child as? MustacheCustomRenderable {
|
||||
return renderable.renderText
|
||||
} else if let lambda = child as? MustacheLambda {
|
||||
return self.renderUnescapedLambda(lambda, parameter: "", context: context)
|
||||
} else {
|
||||
return String(describing: child)
|
||||
}
|
||||
@@ -70,6 +74,9 @@ extension MustacheTemplate {
|
||||
|
||||
case .section(let variable, let transforms, let template):
|
||||
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)
|
||||
|
||||
case .invertedSection(let variable, let transforms, let template):
|
||||
@@ -144,8 +151,6 @@ extension MustacheTemplate {
|
||||
return array.renderSection(with: template, context: context)
|
||||
case let bool as Bool:
|
||||
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:
|
||||
return ""
|
||||
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
|
||||
func getChild(named name: String, transforms: [String], context: MustacheContext) -> Any? {
|
||||
func _getImmediateChild(named name: String, from object: Any) -> Any? {
|
||||
if let customBox = object as? MustacheParent {
|
||||
return customBox.child(named: name)
|
||||
} else {
|
||||
let mirror = Mirror(reflecting: object)
|
||||
return mirror.getValue(forKey: name)
|
||||
}
|
||||
let object = {
|
||||
if let customBox = object as? MustacheParent {
|
||||
return customBox.child(named: name)
|
||||
} else {
|
||||
let mirror = Mirror(reflecting: object)
|
||||
return mirror.getValue(forKey: name)
|
||||
}
|
||||
}()
|
||||
return object
|
||||
}
|
||||
|
||||
func _getChild(named names: ArraySlice<String>, from object: Any) -> Any? {
|
||||
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 }
|
||||
let names2 = names.dropFirst()
|
||||
return _getChild(named: names2, from: childObject)
|
||||
|
||||
@@ -13,12 +13,14 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Class holding Mustache template
|
||||
public struct MustacheTemplate: Sendable {
|
||||
public struct MustacheTemplate: Sendable, CustomStringConvertible {
|
||||
/// Initialize template
|
||||
/// - Parameter string: Template text
|
||||
/// - Throws: MustacheTemplate.Error
|
||||
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
|
||||
}
|
||||
|
||||
@@ -54,12 +56,15 @@ public struct MustacheTemplate: Sendable {
|
||||
return self.render(context: .init(object, library: library))
|
||||
}
|
||||
|
||||
internal init(_ tokens: [Token]) {
|
||||
internal init(_ tokens: [Token], text: String) {
|
||||
self.tokens = tokens
|
||||
self.filename = nil
|
||||
self.text = text
|
||||
}
|
||||
|
||||
enum Token: Sendable {
|
||||
public var description: String { self.text }
|
||||
|
||||
enum Token: Sendable /* , CustomStringConvertible */ {
|
||||
case text(String)
|
||||
case variable(name: String, transforms: [String] = [])
|
||||
case unescapedVariable(name: String, transforms: [String] = [])
|
||||
@@ -73,5 +78,6 @@ public struct MustacheTemplate: Sendable {
|
||||
}
|
||||
|
||||
var tokens: [Token]
|
||||
let text: String
|
||||
let filename: String?
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user