Skip to content

Commit

Permalink
Template inheritance (#9)
Browse files Browse the repository at this point in the history
* Move all context variables into HBMustacheContext

* Add support for reading inherited sections

* Render inherited tokens

* Test inheritance spec, fix two minor issues

* fix warning

* swift format
  • Loading branch information
adam-fowler authored Mar 22, 2021
1 parent af345e9 commit 35d5260
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 59 deletions.
57 changes: 57 additions & 0 deletions Sources/HummingbirdMustache/Context.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
struct HBMustacheContext {
let stack: [Any]
let sequenceContext: HBMustacheSequenceContext?
let indentation: String?
let inherited: [String: HBMustacheTemplate]?

init(_ object: Any) {
self.stack = [object]
self.sequenceContext = nil
self.indentation = nil
self.inherited = nil
}

private init(
stack: [Any],
sequenceContext: HBMustacheSequenceContext?,
indentation: String?,
inherited: [String: HBMustacheTemplate]?
) {
self.stack = stack
self.sequenceContext = sequenceContext
self.indentation = indentation
self.inherited = inherited
}

func withObject(_ object: Any) -> HBMustacheContext {
var stack = self.stack
stack.append(object)
return .init(stack: stack, sequenceContext: nil, indentation: self.indentation, inherited: self.inherited)
}

func withPartial(indented: String?, inheriting: [String: HBMustacheTemplate]?) -> HBMustacheContext {
let indentation: String?
if let indented = indented {
indentation = (self.indentation ?? "") + indented
} else {
indentation = self.indentation
}
let inherits: [String: HBMustacheTemplate]?
if let inheriting = inheriting {
if let originalInherits = self.inherited {
inherits = originalInherits.merging(inheriting) { value, _ in value }
} else {
inherits = inheriting
}
} else {
inherits = self.inherited
}
return .init(stack: self.stack, sequenceContext: nil, indentation: indentation, inherited: inherits)
}

func withSequence(_ object: Any, sequenceContext: HBMustacheSequenceContext) -> HBMustacheContext {
var stack = self.stack
stack.append(object)
return .init(stack: stack, sequenceContext: sequenceContext, indentation: self.indentation, inherited: self.inherited)
}
}
10 changes: 10 additions & 0 deletions Sources/HummingbirdMustache/Library.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ public final class HBMustacheLibrary {
self.templates[name] = template
}

/// Register template under name
/// - Parameters:
/// - mustache: Mustache text
/// - name: Name of template
public func register(_ mustache: String, named name: String) throws {
let template = try HBMustacheTemplate(string: mustache)
template.setLibrary(self)
self.templates[name] = template
}

/// Return template registed with name
/// - Parameter name: name to search for
/// - Returns: Template
Expand Down
33 changes: 13 additions & 20 deletions Sources/HummingbirdMustache/Sequence.swift
Original file line number Diff line number Diff line change
@@ -1,46 +1,39 @@

/// Protocol for objects that can be rendered as a sequence in Mustache
public protocol HBMustacheSequence {
protocol HBMustacheSequence {
/// Render section using template
func renderSection(with template: HBMustacheTemplate, stack: [Any]) -> String
func renderSection(with template: HBMustacheTemplate, context: HBMustacheContext) -> String
/// Render inverted section using template
func renderInvertedSection(with template: HBMustacheTemplate, stack: [Any]) -> String
func renderInvertedSection(with template: HBMustacheTemplate, context: HBMustacheContext) -> String
}

public extension Sequence {
extension Sequence {
/// Render section using template
func renderSection(with template: HBMustacheTemplate, stack: [Any]) -> String {
func renderSection(with template: HBMustacheTemplate, context: HBMustacheContext) -> String {
var string = ""
var context = HBMustacheSequenceContext(first: true)
var sequenceContext = HBMustacheSequenceContext(first: true)

var iterator = makeIterator()
guard var currentObject = iterator.next() else { return "" }

while let object = iterator.next() {
var stack = stack
stack.append(currentObject)
string += template.render(stack, context: context)
string += template.render(context: context.withSequence(currentObject, sequenceContext: sequenceContext))
currentObject = object
context.first = false
context.index += 1
sequenceContext.first = false
sequenceContext.index += 1
}

context.last = true
var stack = stack
stack.append(currentObject)
string += template.render(stack, context: context)
sequenceContext.last = true
string += template.render(context: context.withSequence(currentObject, sequenceContext: sequenceContext))

return string
}

/// Render inverted section using template
func renderInvertedSection(with template: HBMustacheTemplate, stack: [Any]) -> String {
var stack = stack
stack.append(self)

func renderInvertedSection(with template: HBMustacheTemplate, context: HBMustacheContext) -> String {
var iterator = makeIterator()
if iterator.next() == nil {
return template.render(stack)
return template.render(context: context.withObject(self))
}
return ""
}
Expand Down
53 changes: 51 additions & 2 deletions Sources/HummingbirdMustache/Template+Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ extension HBMustacheTemplate {
case expectedSectionEnd
/// set delimiter tag badly formatted
case invalidSetDelimiter
/// cannot apply transform to inherited section
case transformAppliedToInheritanceSection
/// illegal token inside inherit section of partial
case illegalTokenInsideInheritSection
/// text found inside inherit section of partial
case textInsideInheritSection
}

struct ParserState {
Expand Down Expand Up @@ -121,6 +127,21 @@ extension HBMustacheTemplate {
let sectionTokens = try parse(&parser, state: state.withSectionName(name, method: method))
tokens.append(.invertedSection(name: name, method: method, template: HBMustacheTemplate(sectionTokens)))

case "$":
// inherited section
parser.unsafeAdvance()
let (name, method) = try parseName(&parser, state: state)
// ERROR: can't have methods applied to inherited sections
guard method == nil else { throw Error.transformAppliedToInheritanceSection }
if self.isStandalone(&parser, state: state) {
setNewLine = true
} else if whiteSpaceBefore.count > 0 {
tokens.append(.text(String(whiteSpaceBefore)))
whiteSpaceBefore = ""
}
let sectionTokens = try parse(&parser, state: state.withSectionName(name, method: method))
tokens.append(.inheritedSection(name: name, template: HBMustacheTemplate(sectionTokens)))

case "/":
// end of section
parser.unsafeAdvance()
Expand Down Expand Up @@ -174,12 +195,40 @@ extension HBMustacheTemplate {
}
if self.isStandalone(&parser, state: state) {
setNewLine = true
tokens.append(.partial(name, indentation: String(whiteSpaceBefore)))
tokens.append(.partial(name, indentation: String(whiteSpaceBefore), inherits: nil))
} else {
tokens.append(.partial(name, indentation: nil))
tokens.append(.partial(name, indentation: nil, inherits: nil))
}
whiteSpaceBefore = ""

case "<":
// partial with inheritance
parser.unsafeAdvance()
let (name, method) = try parseName(&parser, state: state)
// ERROR: can't have methods applied to inherited sections
guard method == nil else { throw Error.transformAppliedToInheritanceSection }
var indent: String?
if self.isStandalone(&parser, state: state) {
setNewLine = true
} else if whiteSpaceBefore.count > 0 {
indent = String(whiteSpaceBefore)
tokens.append(.text(indent!))
whiteSpaceBefore = ""
}
let sectionTokens = try parse(&parser, state: state.withSectionName(name, method: method))
var inherit: [String: HBMustacheTemplate] = [:]
for token in sectionTokens {
switch token {
case .inheritedSection(let name, let template):
inherit[name] = template
case .text:
break
default:
throw Error.illegalTokenInsideInheritSection
}
}
tokens.append(.partial(name, indentation: indent, inherits: inherit))

case "=":
// set delimiter
parser.unsafeAdvance()
Expand Down
61 changes: 34 additions & 27 deletions Sources/HummingbirdMustache/Template+Render.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,57 @@ extension HBMustacheTemplate {
/// - context: Context that render is occurring in. Contains information about position in sequence
/// - indentation: indentation of partial
/// - Returns: Rendered text
func render(_ stack: [Any], context: HBMustacheSequenceContext? = nil, indentation: String? = nil) -> String {
func render(context: HBMustacheContext) -> String {
var string = ""
if let indentation = indentation, indentation != "" {
if let indentation = context.indentation, indentation != "" {
for token in tokens {
if string.last == "\n" {
string += indentation
}
string += self.renderToken(token, stack: stack, context: context)
string += self.renderToken(token, context: context)
}
} else {
for token in tokens {
string += self.renderToken(token, stack: stack, context: context)
string += self.renderToken(token, context: context)
}
}
return string
}

func renderToken(_ token: Token, stack: [Any], context: HBMustacheSequenceContext? = nil) -> String {
func renderToken(_ token: Token, context: HBMustacheContext) -> String {
switch token {
case .text(let text):
return text
case .variable(let variable, let method):
if let child = getChild(named: variable, from: stack, method: method, context: context) {
if let child = getChild(named: variable, method: method, context: context) {
if let template = child as? HBMustacheTemplate {
return template.render(stack)
return template.render(context: context)
} else {
return String(describing: child).htmlEscape()
}
}
case .unescapedVariable(let variable, let method):
if let child = getChild(named: variable, from: stack, method: method, context: context) {
if let child = getChild(named: variable, method: method, context: context) {
return String(describing: child)
}
case .section(let variable, let method, let template):
let child = self.getChild(named: variable, from: stack, method: method, context: context)
return self.renderSection(child, stack: stack, with: template)
let child = self.getChild(named: variable, method: method, context: context)
return self.renderSection(child, with: template, context: context)

case .invertedSection(let variable, let method, let template):
let child = self.getChild(named: variable, from: stack, method: method, context: context)
return self.renderInvertedSection(child, stack: stack, with: template)
let child = self.getChild(named: variable, method: method, context: context)
return self.renderInvertedSection(child, with: template, context: context)

case .partial(let name, let indentation):
case .inheritedSection(let name, let template):
if let override = context.inherited?[name] {
return override.render(context: context)
} else {
return template.render(context: context)
}

case .partial(let name, let indentation, let overrides):
if let template = library?.getTemplate(named: name) {
return template.render(stack, indentation: indentation)
return template.render(context: context.withPartial(indented: indentation, inheriting: overrides))
}
}
return ""
Expand All @@ -61,16 +68,16 @@ extension HBMustacheTemplate {
/// - parent: Current object being rendered
/// - template: Template to render with
/// - Returns: Rendered text
func renderSection(_ child: Any?, stack: [Any], with template: HBMustacheTemplate) -> String {
func renderSection(_ child: Any?, with template: HBMustacheTemplate, context: HBMustacheContext) -> String {
switch child {
case let array as HBMustacheSequence:
return array.renderSection(with: template, stack: stack + [array])
return array.renderSection(with: template, context: context)
case let bool as Bool:
return bool ? template.render(stack) : ""
return bool ? template.render(context: context) : ""
case let lambda as HBMustacheLambda:
return lambda.run(stack.last!, template)
return lambda.run(context.stack.last!, template)
case .some(let value):
return template.render(stack + [value])
return template.render(context: context.withObject(value))
case .none:
return ""
}
Expand All @@ -82,21 +89,21 @@ extension HBMustacheTemplate {
/// - parent: Current object being rendered
/// - template: Template to render with
/// - Returns: Rendered text
func renderInvertedSection(_ child: Any?, stack: [Any], with template: HBMustacheTemplate) -> String {
func renderInvertedSection(_ child: Any?, with template: HBMustacheTemplate, context: HBMustacheContext) -> String {
switch child {
case let array as HBMustacheSequence:
return array.renderInvertedSection(with: template, stack: stack)
return array.renderInvertedSection(with: template, context: context)
case let bool as Bool:
return bool ? "" : template.render(stack)
return bool ? "" : template.render(context: context)
case .some:
return ""
case .none:
return template.render(stack)
return template.render(context: context)
}
}

/// Get child object from variable name
func getChild(named name: String, from stack: [Any], method: String?, context: HBMustacheSequenceContext?) -> Any? {
func getChild(named name: String, method: String?, context: HBMustacheContext) -> Any? {
func _getImmediateChild(named name: String, from object: Any) -> Any? {
if let customBox = object as? HBMustacheParent {
return customBox.child(named: name)
Expand Down Expand Up @@ -129,12 +136,12 @@ extension HBMustacheTemplate {
// the name is split by "." and we use mirror to get the correct child object
let child: Any?
if name == "." {
child = stack.last!
child = context.stack.last!
} else if name == "", method != nil {
child = context
child = context.sequenceContext
} else {
let nameSplit = name.split(separator: ".").map { String($0) }
child = _getChildInStack(named: nameSplit[...], from: stack)
child = _getChildInStack(named: nameSplit[...], from: context.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
Expand Down
9 changes: 6 additions & 3 deletions Sources/HummingbirdMustache/Template.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public final class HBMustacheTemplate {
/// - Parameter object: Object to render
/// - Returns: Rendered text
public func render(_ object: Any) -> String {
self.render([object], context: nil)
self.render(context: .init(object))
}

internal init(_ tokens: [Token]) {
Expand All @@ -22,8 +22,10 @@ public final class HBMustacheTemplate {
self.library = library
for token in self.tokens {
switch token {
case .section(_, _, let template), .invertedSection(_, _, let template):
case .section(_, _, let template), .invertedSection(_, _, let template), .inheritedSection(_, let template):
template.setLibrary(library)
case .partial(_, _, let templates):
templates?.forEach { $1.setLibrary(library) }
default:
break
}
Expand All @@ -36,7 +38,8 @@ public final class HBMustacheTemplate {
case unescapedVariable(name: String, method: String? = nil)
case section(name: String, method: String? = nil, template: HBMustacheTemplate)
case invertedSection(name: String, method: String? = nil, template: HBMustacheTemplate)
case partial(String, indentation: String?)
case inheritedSection(name: String, template: HBMustacheTemplate)
case partial(String, indentation: String?, inherits: [String: HBMustacheTemplate]?)
}

let tokens: [Token]
Expand Down
Loading

0 comments on commit 35d5260

Please sign in to comment.