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:
@@ -232,29 +232,55 @@ extension MustacheTemplate {
|
|||||||
case ">":
|
case ">":
|
||||||
// partial
|
// partial
|
||||||
parser.unsafeAdvance()
|
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)
|
let name = try parsePartialName(&parser, state: state)
|
||||||
if whiteSpaceBefore.count > 0 {
|
if whiteSpaceBefore.count > 0 {
|
||||||
tokens.append(.text(String(whiteSpaceBefore)))
|
tokens.append(.text(String(whiteSpaceBefore)))
|
||||||
}
|
}
|
||||||
if self.isStandalone(&parser, state: state) {
|
if self.isStandalone(&parser, state: state) {
|
||||||
setNewLine = true
|
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 {
|
} 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 = ""
|
whiteSpaceBefore = ""
|
||||||
|
|
||||||
case "<":
|
case "<":
|
||||||
// partial with inheritance
|
// partial with inheritance
|
||||||
parser.unsafeAdvance()
|
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 {
|
if whiteSpaceBefore.count > 0 {
|
||||||
tokens.append(.text(String(whiteSpaceBefore)))
|
tokens.append(.text(String(whiteSpaceBefore)))
|
||||||
}
|
}
|
||||||
if self.isStandalone(&parser, state: state) {
|
if self.isStandalone(&parser, state: state) {
|
||||||
setNewLine = true
|
setNewLine = true
|
||||||
}
|
}
|
||||||
let sectionTokens = try parse(&parser, state: state.withInheritancePartial(name))
|
let sectionTokens = try parse(&parser, state: state.withInheritancePartial(sectionName))
|
||||||
var inherit: [String: MustacheTemplate] = [:]
|
var inherit: [String: MustacheTemplate] = [:]
|
||||||
// parse tokens in section to extract inherited sections
|
// parse tokens in section to extract inherited sections
|
||||||
for token in sectionTokens {
|
for token in sectionTokens {
|
||||||
@@ -267,7 +293,11 @@ extension MustacheTemplate {
|
|||||||
throw Error.illegalTokenInsideInheritSection
|
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 = ""
|
whiteSpaceBefore = ""
|
||||||
|
|
||||||
case "$":
|
case "$":
|
||||||
@@ -478,7 +508,7 @@ extension MustacheTemplate {
|
|||||||
return state.newLine && self.hasLineFinished(&parser)
|
return state.newLine && self.hasLineFinished(&parser)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static let sectionNameCharsWithoutBrackets = Set<Character>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_?")
|
private static let sectionNameCharsWithoutBrackets = Set<Character>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_?*")
|
||||||
private static let sectionNameChars = Set<Character>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_?()")
|
private static let sectionNameChars = Set<Character>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_?()*")
|
||||||
private static let partialNameChars = Set<Character>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_()")
|
private static let partialNameChars = Set<Character>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_()")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,28 @@ extension MustacheTemplate {
|
|||||||
return template.render(context: context.withPartial(indented: indentation, inheriting: overrides))
|
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):
|
case .contentType(let contentType):
|
||||||
context = context.withContentType(contentType)
|
context = context.withContentType(contentType)
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ public struct MustacheTemplate: Sendable {
|
|||||||
case blockDefinition(name: String, template: MustacheTemplate)
|
case blockDefinition(name: String, template: MustacheTemplate)
|
||||||
case blockExpansion(name: String, default: MustacheTemplate, indentation: String?)
|
case blockExpansion(name: String, default: MustacheTemplate, indentation: String?)
|
||||||
case partial(String, indentation: String?, inherits: [String: MustacheTemplate]?)
|
case partial(String, indentation: String?, inherits: [String: MustacheTemplate]?)
|
||||||
|
case dynamicNamePartial(String, indentation: String?, inherits: [String: MustacheTemplate]?)
|
||||||
case contentType(MustacheContentType)
|
case contentType(MustacheContentType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ final class MustacheSpecTests: XCTestCase {
|
|||||||
let expected: String
|
let expected: String
|
||||||
|
|
||||||
func run() throws {
|
func run() throws {
|
||||||
// print("Test: \(self.name)")
|
print("Test: \(self.name)")
|
||||||
if let partials = self.partials {
|
if let partials = self.partials {
|
||||||
let template = try MustacheTemplate(string: self.template)
|
let template = try MustacheTemplate(string: self.template)
|
||||||
var templates: [String: MustacheTemplate] = ["__test__": 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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'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
|
/// test MustacheCustomRenderable
|
||||||
func testCustomRenderable() throws {
|
func testCustomRenderable() throws {
|
||||||
let template = try MustacheTemplate(string: "{{.}}")
|
let template = try MustacheTemplate(string: "{{.}}")
|
||||||
|
|||||||
Reference in New Issue
Block a user