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: "{{.}}")