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:
Adam Fowler
2024-09-19 17:17:50 +01:00
committed by GitHub
parent 8fba85e28c
commit 933fa3d60f
9 changed files with 249 additions and 44 deletions

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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?
}