Template inheritance (#9)
* 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
This commit is contained in:
57
Sources/HummingbirdMustache/Context.swift
Normal file
57
Sources/HummingbirdMustache/Context.swift
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -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 ""
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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]) {
|
||||
@@ -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
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -51,4 +51,51 @@ final class PartialTests: XCTestCase {
|
||||
|
||||
""")
|
||||
}
|
||||
|
||||
/// test inheritance
|
||||
func testInheritance() throws {
|
||||
let library = HBMustacheLibrary()
|
||||
try library.register(
|
||||
"""
|
||||
<head>
|
||||
<title>{{$title}}Default title{{/title}}</title>
|
||||
</head>
|
||||
|
||||
""",
|
||||
named: "header"
|
||||
)
|
||||
try library.register(
|
||||
"""
|
||||
<html>
|
||||
{{$header}}{{/header}}
|
||||
{{$content}}{{/content}}
|
||||
</html>
|
||||
|
||||
""",
|
||||
named: "base"
|
||||
)
|
||||
try library.register(
|
||||
"""
|
||||
{{<base}}
|
||||
{{$header}}
|
||||
{{<header}}
|
||||
{{$title}}My page title{{/title}}
|
||||
{{/header}}
|
||||
{{/header}}
|
||||
{{$content}}<h1>Hello world</h1>{{/content}}
|
||||
{{/base}}
|
||||
|
||||
""",
|
||||
named: "mypage"
|
||||
)
|
||||
XCTAssertEqual(library.render({}, withTemplate: "mypage")!, """
|
||||
<html>
|
||||
<head>
|
||||
<title>My page title</title>
|
||||
</head>
|
||||
<h1>Hello world</h1>
|
||||
</html>
|
||||
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,11 +46,6 @@ public extension AnyDecodable {
|
||||
/// Verify implementation against formal standard for Mustache.
|
||||
/// https://github.com/mustache/spec
|
||||
final class MustacheSpecTests: XCTestCase {
|
||||
func loadSpec(name: String) throws -> Data {
|
||||
let url = URL(string: "https://raw.githubusercontent.com/mustache/spec/master/specs/\(name).json")!
|
||||
return try Data(contentsOf: url)
|
||||
}
|
||||
|
||||
struct Spec: Decodable {
|
||||
struct Test: Decodable {
|
||||
let name: String
|
||||
@@ -91,7 +86,12 @@ final class MustacheSpecTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testSpec(name: String, ignoring: [String] = []) throws {
|
||||
let data = try loadSpec(name: name)
|
||||
let url = URL(string: "https://raw.githubusercontent.com/mustache/spec/master/specs/\(name).json")!
|
||||
try testSpec(url: url, ignoring: ignoring)
|
||||
}
|
||||
|
||||
func testSpec(url: URL, ignoring: [String] = []) throws {
|
||||
let data = try Data(contentsOf: url)
|
||||
let spec = try JSONDecoder().decode(Spec.self, from: data)
|
||||
|
||||
print(spec.overview)
|
||||
@@ -124,4 +124,11 @@ final class MustacheSpecTests: XCTestCase {
|
||||
func testSectionsSpec() throws {
|
||||
try self.testSpec(name: "sections", ignoring: ["Variable test"])
|
||||
}
|
||||
|
||||
func testInheritanceSpec() throws {
|
||||
let url = URL(
|
||||
string: "https://raw.githubusercontent.com/mustache/spec/ab227509e64961943ca374c09c08b63f59da014a/specs/inheritance.json"
|
||||
)!
|
||||
try self.testSpec(url: url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ extension HBMustacheTemplate.Token: Equatable {
|
||||
return lhs1 == rhs1 && lhs2 == rhs2 && lhs3 == rhs3
|
||||
case (.invertedSection(let lhs1, let lhs2, let lhs3), .invertedSection(let rhs1, let rhs2, let rhs3)):
|
||||
return lhs1 == rhs1 && lhs2 == rhs2 && lhs3 == rhs3
|
||||
case (.partial(let name1, let indent1), .partial(let name2, let indent2)):
|
||||
case (.partial(let name1, let indent1, _), .partial(let name2, let indent2, _)):
|
||||
return name1 == name2 && indent1 == indent2
|
||||
default:
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user