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
This commit is contained in:
Adam Fowler
2024-08-28 08:31:06 +01:00
committed by GitHub
parent a010f172c5
commit 01b1f21ed6
5 changed files with 178 additions and 8 deletions

View File

@@ -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<Character>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_?")
private static let sectionNameChars = Set<Character>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_?()")
private static let sectionNameCharsWithoutBrackets = Set<Character>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_?*")
private static let sectionNameChars = Set<Character>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_?()*")
private static let partialNameChars = Set<Character>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_()")
}

View File

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

View File

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

View File

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

View File

@@ -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: """
<h1>{{$title}}The News of Today{{/title}}</h1>
{{$body}}
<p>Nothing special happened.</p>
{{/body}}
""")
XCTAssertEqual(template.render([]), """
<h1>The News of Today</h1>
<p>Nothing special happened.</p>
""")
}
func testMustacheManualParents() throws {
var library = MustacheLibrary()
try library.register(
"""
{{<article}}
Never shown
{{$body}}
{{#headlines}}
<p>{{.}}</p>
{{/headlines}}
{{/body}}
{{/article}}
{{<article}}
{{$title}}Yesterday{{/title}}
{{/article}}
""",
named: "main"
)
try library.register(
"""
<h1>{{$title}}The News of Today{{/title}}</h1>
{{$body}}
<p>Nothing special happened.</p>
{{/body}}
""",
named: "article"
)
let object = [
"headlines": [
"A pug's handler grew mustaches.",
"What an exciting day!",
],
]
XCTAssertEqual(
library.render(object, withTemplate: "main"),
"""
<h1>The News of Today</h1>
<p>A pug&#39;s handler grew mustaches.</p>
<p>What an exciting day!</p>
<h1>Yesterday</h1>
<p>Nothing special happened.</p>
"""
)
}
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(
"""
<b>{{$text}}Here also goes nothing but it's bold.{{/text}}</b>
""",
named: "bold"
)
let object = ["dynamic": "bold"]
XCTAssertEqual(
library.render(object, withTemplate: "dynamic"),
"""
<b>Hello World!</b>
"""
)
}
/// test MustacheCustomRenderable
func testCustomRenderable() throws {
let template = try MustacheTemplate(string: "{{.}}")