From 01b1f21ed6ac822794012273b26c524fea28c749 Mon Sep 17 00:00:00 2001
From: Adam Fowler
Date: Wed, 28 Aug 2024 08:31:06 +0100
Subject: [PATCH] Dynamic names support (#49)
* Dynamic names support
* Add support for dynamic names in parent tags
* Support all dynamic names spec
* Swift 5.8 compile fix
---
Sources/Mustache/Template+Parser.swift | 44 +++++--
Sources/Mustache/Template+Render.swift | 22 ++++
Sources/Mustache/Template.swift | 1 +
Tests/MustacheTests/SpecTests.swift | 6 +-
.../MustacheTests/TemplateRendererTests.swift | 113 ++++++++++++++++++
5 files changed, 178 insertions(+), 8 deletions(-)
diff --git a/Sources/Mustache/Template+Parser.swift b/Sources/Mustache/Template+Parser.swift
index 0e61ea2..335d124 100644
--- a/Sources/Mustache/Template+Parser.swift
+++ b/Sources/Mustache/Template+Parser.swift
@@ -232,29 +232,55 @@ extension MustacheTemplate {
case ">":
// partial
parser.unsafeAdvance()
+ // skip whitespace
+ parser.read(while: \.isWhitespace)
+ var dynamic = false
+ if parser.current() == "*" {
+ parser.unsafeAdvance()
+ dynamic = true
+ }
let name = try parsePartialName(&parser, state: state)
if whiteSpaceBefore.count > 0 {
tokens.append(.text(String(whiteSpaceBefore)))
}
if self.isStandalone(&parser, state: state) {
setNewLine = true
- tokens.append(.partial(name, indentation: String(whiteSpaceBefore), inherits: nil))
+ if dynamic {
+ tokens.append(.dynamicNamePartial(name, indentation: String(whiteSpaceBefore), inherits: nil))
+ } else {
+ tokens.append(.partial(name, indentation: String(whiteSpaceBefore), inherits: nil))
+ }
} else {
- tokens.append(.partial(name, indentation: nil, inherits: nil))
+ if dynamic {
+ tokens.append(.dynamicNamePartial(name, indentation: nil, inherits: nil))
+ } else {
+ tokens.append(.partial(name, indentation: nil, inherits: nil))
+ }
}
whiteSpaceBefore = ""
case "<":
// partial with inheritance
parser.unsafeAdvance()
- let name = try parsePartialName(&parser, state: state)
+ // skip whitespace
+ parser.read(while: \.isWhitespace)
+ let sectionName = try parsePartialName(&parser, state: state)
+ let name: String
+ let dynamic: Bool
+ if sectionName.first == "*" {
+ dynamic = true
+ name = String(sectionName.dropFirst())
+ } else {
+ dynamic = false
+ name = sectionName
+ }
if whiteSpaceBefore.count > 0 {
tokens.append(.text(String(whiteSpaceBefore)))
}
if self.isStandalone(&parser, state: state) {
setNewLine = true
}
- let sectionTokens = try parse(&parser, state: state.withInheritancePartial(name))
+ let sectionTokens = try parse(&parser, state: state.withInheritancePartial(sectionName))
var inherit: [String: MustacheTemplate] = [:]
// parse tokens in section to extract inherited sections
for token in sectionTokens {
@@ -267,7 +293,11 @@ extension MustacheTemplate {
throw Error.illegalTokenInsideInheritSection
}
}
- tokens.append(.partial(name, indentation: String(whiteSpaceBefore), inherits: inherit))
+ if dynamic {
+ tokens.append(.dynamicNamePartial(name, indentation: String(whiteSpaceBefore), inherits: inherit))
+ } else {
+ tokens.append(.partial(name, indentation: String(whiteSpaceBefore), inherits: inherit))
+ }
whiteSpaceBefore = ""
case "$":
@@ -478,7 +508,7 @@ extension MustacheTemplate {
return state.newLine && self.hasLineFinished(&parser)
}
- private static let sectionNameCharsWithoutBrackets = Set("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_?")
- private static let sectionNameChars = Set("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_?()")
+ private static let sectionNameCharsWithoutBrackets = Set("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_?*")
+ private static let sectionNameChars = Set("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_?()*")
private static let partialNameChars = Set("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_()")
}
diff --git a/Sources/Mustache/Template+Render.swift b/Sources/Mustache/Template+Render.swift
index 6862d23..0a358a9 100644
--- a/Sources/Mustache/Template+Render.swift
+++ b/Sources/Mustache/Template+Render.swift
@@ -101,6 +101,28 @@ extension MustacheTemplate {
return template.render(context: context.withPartial(indented: indentation, inheriting: overrides))
}
+ case .dynamicNamePartial(let name, let indentation, let overrides):
+ let child = self.getChild(named: name, transforms: [], context: context)
+ guard let childName = child as? String else {
+ return ""
+ }
+ if var template = context.library?.getTemplate(named: childName) {
+ #if DEBUG
+ if context.reloadPartials {
+ guard let filename = template.filename else {
+ preconditionFailure("Can only use reload if template was generated from a file")
+ }
+ do {
+ guard let partialTemplate = try MustacheTemplate(filename: filename) else { return "Cannot find template at \(filename)" }
+ template = partialTemplate
+ } catch {
+ return "\(error)"
+ }
+ }
+ #endif
+ return template.render(context: context.withPartial(indented: indentation, inheriting: overrides))
+ }
+
case .contentType(let contentType):
context = context.withContentType(contentType)
diff --git a/Sources/Mustache/Template.swift b/Sources/Mustache/Template.swift
index 26a1211..72574a2 100644
--- a/Sources/Mustache/Template.swift
+++ b/Sources/Mustache/Template.swift
@@ -68,6 +68,7 @@ public struct MustacheTemplate: Sendable {
case blockDefinition(name: String, template: MustacheTemplate)
case blockExpansion(name: String, default: MustacheTemplate, indentation: String?)
case partial(String, indentation: String?, inherits: [String: MustacheTemplate]?)
+ case dynamicNamePartial(String, indentation: String?, inherits: [String: MustacheTemplate]?)
case contentType(MustacheContentType)
}
diff --git a/Tests/MustacheTests/SpecTests.swift b/Tests/MustacheTests/SpecTests.swift
index e9400d4..de2e33d 100644
--- a/Tests/MustacheTests/SpecTests.swift
+++ b/Tests/MustacheTests/SpecTests.swift
@@ -69,7 +69,7 @@ final class MustacheSpecTests: XCTestCase {
let expected: String
func run() throws {
- // print("Test: \(self.name)")
+ print("Test: \(self.name)")
if let partials = self.partials {
let template = try MustacheTemplate(string: self.template)
var templates: [String: MustacheTemplate] = ["__test__": template]
@@ -188,4 +188,8 @@ final class MustacheSpecTests: XCTestCase {
]
)
}
+
+ func testDynamicNamesSpec() async throws {
+ try await self.testSpec(name: "~dynamic-names")
+ }
}
diff --git a/Tests/MustacheTests/TemplateRendererTests.swift b/Tests/MustacheTests/TemplateRendererTests.swift
index 66c7852..8e85d58 100644
--- a/Tests/MustacheTests/TemplateRendererTests.swift
+++ b/Tests/MustacheTests/TemplateRendererTests.swift
@@ -261,6 +261,119 @@ final class TemplateRendererTests: XCTestCase {
""")
}
+ /// test dynamic names
+ func testMustacheManualDynamicNames() throws {
+ var library = MustacheLibrary()
+ try library.register(
+ "Hello {{>*dynamic}}",
+ named: "main"
+ )
+ try library.register(
+ "everyone!",
+ named: "world"
+ )
+ let object = ["dynamic": "world"]
+ XCTAssertEqual(library.render(object, withTemplate: "main"), "Hello everyone!")
+ }
+
+ /// test block with defaults
+ func testMustacheManualBlocksWithDefaults() throws {
+ let template = try MustacheTemplate(string: """
+ {{$title}}The News of Today{{/title}}
+ {{$body}}
+ Nothing special happened.
+ {{/body}}
+
+ """)
+ XCTAssertEqual(template.render([]), """
+ The News of Today
+ Nothing special happened.
+
+ """)
+ }
+
+ func testMustacheManualParents() throws {
+ var library = MustacheLibrary()
+ try library.register(
+ """
+ {{{{.}}
+ {{/headlines}}
+ {{/body}}
+ {{/article}}
+
+ {{{{$title}}The News of Today{{/title}}
+ {{$body}}
+ Nothing special happened.
+ {{/body}}
+
+ """,
+ named: "article"
+ )
+ let object = [
+ "headlines": [
+ "A pug's handler grew mustaches.",
+ "What an exciting day!",
+ ],
+ ]
+ XCTAssertEqual(
+ library.render(object, withTemplate: "main"),
+ """
+ The News of Today
+ A pug's handler grew mustaches.
+ What an exciting day!
+
+ Yesterday
+ Nothing special happened.
+
+ """
+ )
+ }
+
+ func testMustacheManualDynamicNameParents() throws {
+ var library = MustacheLibrary()
+ try library.register(
+ """
+ {{<*dynamic}}
+ {{$text}}Hello World!{{/text}}
+ {{/*dynamic}}
+
+ """,
+ named: "dynamic"
+ )
+ try library.register(
+ """
+ {{$text}}Here goes nothing.{{/text}}
+ """,
+ named: "normal"
+ )
+ try library.register(
+ """
+ {{$text}}Here also goes nothing but it's bold.{{/text}}
+ """,
+ named: "bold"
+ )
+ let object = ["dynamic": "bold"]
+ XCTAssertEqual(
+ library.render(object, withTemplate: "dynamic"),
+ """
+ Hello World!
+ """
+ )
+ }
+
/// test MustacheCustomRenderable
func testCustomRenderable() throws {
let template = try MustacheTemplate(string: "{{.}}")